{"id":237,"date":"2021-07-24T15:09:47","date_gmt":"2021-07-24T14:09:47","guid":{"rendered":"https:\/\/www.julianmejio.com\/blog\/?p=237"},"modified":"2021-07-24T15:09:47","modified_gmt":"2021-07-24T14:09:47","slug":"implementacion-de-un-servicio-de-traduccion-simple-y-efectivo-en-javascript","status":"publish","type":"post","link":"https:\/\/www.julianmejio.com\/blog\/2021\/07\/24\/implementacion-de-un-servicio-de-traduccion-simple-y-efectivo-en-javascript\/","title":{"rendered":"Implementaci\u00f3n de un servicio de traducci\u00f3n simple y efectivo en Javascript"},"content":{"rendered":"\n<p>Esta aproximaci\u00f3n tiene el objetivo de implementar un servicio estricto de traducci\u00f3n dentro de aplicaciones Javascript y con muy pocos artefactos.<\/p>\n\n\n\n<p>Durante todo este a\u00f1o, y para mi sorpresa, fui embarcado en un proyecto donde el 80% del c\u00f3digo estaba escrito en Typescript. Esto aumento mi inter\u00e9s en el lenguaje, ya que ofrece una capa de reglas estrictas \u2014y algunas veces exageradas\u2014 a Javascript. Sin embargo, desde siempre he sentido que la internacionalizaci\u00f3n de las aplicaciones en Javascript no es consistente. He tenido la experiencia de ver cuanto intento de traducci\u00f3n \u2014algunos nefastos\u2014 en este lenguaje.<\/p>\n\n\n\n<p>Es por eso que decid\u00ed implementar un servicio de traducci\u00f3n bastante rudimentario y simple que pudiera manejar correctamente las traducciones dentro de toda la aplicaci\u00f3n. Aunque el stack en donde lo he implementado es h\u00edbrido, el servicio de traducci\u00f3n es completamente portable.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">Modelo<\/h2>\n\n\n\n<p>La implementaci\u00f3n ofrece simpleza y flexibilidad. Se trata de una clase facade principal compuesta de diccionarios, uno por cada idioma. La idea principal es estandarizar los mensajes por medio de claves para que puedan ser reemplazados seg\u00fan el idioma demandado.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" data-attachment-id=\"245\" data-permalink=\"https:\/\/www.julianmejio.com\/blog\/2021\/07\/24\/implementacion-de-un-servicio-de-traduccion-simple-y-efectivo-en-javascript\/translator_service-01\/\" data-orig-file=\"https:\/\/www.julianmejio.com\/blog\/wp-content\/uploads\/2021\/07\/translator_service-01.png\" data-orig-size=\"978,725\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"translator_service-01\" data-image-description=\"\" data-image-caption=\"\" data-medium-file=\"https:\/\/www.julianmejio.com\/blog\/wp-content\/uploads\/2021\/07\/translator_service-01-300x222.png\" data-large-file=\"https:\/\/www.julianmejio.com\/blog\/wp-content\/uploads\/2021\/07\/translator_service-01.png\" src=\"https:\/\/www.julianmejio.com\/blog\/wp-content\/uploads\/2021\/07\/translator_service-01.png\" alt=\"\" class=\"wp-image-245\" width=\"489\" height=\"363\" srcset=\"https:\/\/www.julianmejio.com\/blog\/wp-content\/uploads\/2021\/07\/translator_service-01.png 978w, https:\/\/www.julianmejio.com\/blog\/wp-content\/uploads\/2021\/07\/translator_service-01-300x222.png 300w, https:\/\/www.julianmejio.com\/blog\/wp-content\/uploads\/2021\/07\/translator_service-01-768x569.png 768w\" sizes=\"auto, (max-width: 489px) 100vw, 489px\" \/><figcaption>Modelo del servicio de traducci\u00f3n para Javascript<\/figcaption><\/figure>\n\n\n\n<p>La clase <em>facade<\/em> Translator es est\u00e1tica, lo que permite el uso f\u00e1cil sobre todo el alcance de la agregaci\u00f3n. Los puntos principales que tuve en cuenta fueron:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Configuraci\u00f3n regional por defecto: representado por el atributo <em>string<\/em> <em>localeDefault<\/em>. Este atributo decidir\u00e1 cu\u00e1l debe ser el diccionario de idioma que se usar\u00e1 por defecto. Aunque dentro de este modelo la decisi\u00f3n es difusa, en la implementaci\u00f3n que proporcionar\u00e9 m\u00e1s adelante tendr\u00e9 en cuenta un valor obtenido por una variable de entorno y tambi\u00e9n por un valor <em>fallback<\/em> incrustado directamente en el c\u00f3digo, cuanod nada m\u00e1s est\u00e9 disponible.<\/li><li>Una forma sencilla de recuperar el diccionario de idioma: esta est\u00e1 representada por la operaci\u00f3n <em>ITranslationDictionary? getDictionary(string?)<\/em>. Dentro de la implementaci\u00f3n, esta operaci\u00f3n retorna el diccionario asociado a <em>localeDefault<\/em>; y si este no existe, retorna un valor nulo. El par\u00e1metro <em>locale<\/em> es opcional, y en caso de no ser especificado, usar\u00e1 el valor almacenado en <em>localeDefault<\/em>.<\/li><li>Una operaci\u00f3n para traducir: esta est\u00e1 representada por la operaci\u00f3n <em>string trans(string, string?)<\/em>. Esta basada en casi todos los paquetes de traducci\u00f3n que se pueden encontrar en el mercado, y est\u00e1 pensada para usarse solo con la clave de traducci\u00f3n. Si se usa el segundo par\u00e1metro, el diccionario que se usar\u00e1 no ser\u00e1 el valor por defecto sino del valor que se especifique ah\u00ed. Esta operaci\u00f3n siempre retornar\u00e1 un string; esto maximiza la compatibilidad. En caso de que el diccionario o la entrada dentro de este no exista, la clave es la cadena que se retorna.<\/li><\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Garantizar la consistencia de los diccionarios<\/h3>\n\n\n\n<p>Cada diccionario representa un idioma y es independiente a los dem\u00e1s, sin embargo esto podr\u00eda dar lugar a inconsistencias como que no todas las claves est\u00e9n presentes en alg\u00fan diccionario. Aunque por la forma de construcci\u00f3n de la clase Translator esto no supone un problema l\u00f3gico, no pasa lo mismo con un error humano, que facilmente puede olvidar alguna clave. Para solucionar este problema hago uso de una interfaz <em>ITranslationDictionary<\/em>, la cual sirve de enumerador de todas las claves de traducci\u00f3n a usar, y cada diccionario debe implementarla.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Implementaci\u00f3n<\/h2>\n\n\n\n<p>La implementaci\u00f3n m\u00ednima  consta de tres artefactos: una clase facade, una interfaz ITranslationDictionary y al menos una clase que implemente dicha interfaz. Vale la pena resaltar que aqu\u00ed hago uso de algunas instrucciones espec\u00edficas de Typescript, sin embargo puede convertirse facilmente a Javascript puro.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Clase <em>facade<\/em> Translator<\/h3>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-ts\" data-file=\"translator\/Translator.ts\" data-lang=\"TypeScript\"><code>import { TranslationDictionaryInterface } from &#39;.\/TranslationDictionaryInterface&#39;\nimport EsDictionary from &#39;.\/dictionary\/EsDictionary&#39;\n\nconst defaultLocale: string = process.env.APP_LOCALE || &#39;es&#39;\n\nexport default class Translator {\n    private static dictionaries: Record&lt;string, TranslationDictionaryInterface&gt; = {\n      es: new EsDictionary()\n    };\n\n    private static getDictionary (locale?: string): TranslationDictionaryInterface | null {\n      return Translator.dictionaries[locale || defaultLocale] || null\n    }\n\n    public static trans (translationKey: string, locale?: string): string {\n      const dictionary: TranslationDictionaryInterface | null = this.getDictionary(locale)\n      if (!dictionary) {\n        console.warn(&#39;Translation dictionary not found&#39;, locale || defaultLocale)\n        return translationKey\n      }\n      const translation: string | null = dictionary[translationKey] as string | null\n      if (translation == null) {\n        console.warn(&#39;Translation key not found on dictionary&#39;, translationKey, locale || defaultLocale)\n      }\n      return translation || translationKey\n    }\n}\n<\/code><\/pre><\/div>\n\n\n\n<p>Para determinar la configuraci\u00f3n regional por defecto hago uso de variables de entorno y un valor incrustado a manera de fallback. En caso de tener una aplicaci\u00f3n que defina de manera din\u00e1mica su configuraci\u00f3n regional, por ejemplo, en alg\u00fan par\u00e1metro de URL, se puede cambiar facilmente por otro m\u00e9todo que se ajuste a las medidas.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Diccionarios est\u00e1ticamente incrustados vs. cargados din\u00e1micamente<\/h4>\n\n\n\n<p>La raz\u00f3n por la que la lista de diccionarios en la variable <em>dictionaries<\/em> es construida de manera est\u00e1tica, es que de ese modo garantizamos que el c\u00f3digo no sea mutable o que haya un riesgo mayor de inyecci\u00f3n de c\u00f3digo. Al principio us\u00e9 importaciones din\u00e1micas basadas en la configuraci\u00f3n regional \u2014cargar los diccionarios bajo demanda\u2014. A primera vista ofrece una carga m\u00e1s r\u00e1pida debido a que los diccionarios se cargan bajo demanda y puede ser cacheables, pero a costa de una configuraci\u00f3n de compilaci\u00f3n m\u00e1s compleja y ampliaci\u00f3n de la superficie de ataque por medio de XSS. Adem\u00e1s, hay m\u00e1s consumo de recursos en esa petici\u00f3n HTTP adicional que si el diccionario estuviera ya incrustado. Por lo que al final me decid\u00ed por simplificar la clase y a\u00f1adir una lista est\u00e1tica.<\/p>\n\n\n\n<p>Otra ventaja de los diccionarios incrustados, es que la clase no es as\u00edncrona. Cuando prob\u00e9 la carga din\u00e1mica, la clase tuvo que adaptarse al modelo as\u00edncrono <em>async<\/em>\/<em>await<\/em>. Esto quiere decir que si hay un problema de red o demora en el hilo de traducci\u00f3n, el usuario final no podr\u00e1 ver los mensajes inmediatamente, y podr\u00eda incluso resultar en un problema de usabilidad. Por lo tanto, esta soluci\u00f3n de carga din\u00e1mica no la recomiendo para este tipo de servicios cr\u00edticos.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Acceso a los atributos de la clase <em>TranslationDictionaryInterface<\/em><\/h4>\n\n\n\n<p>En la l\u00ednea 21 se accede a las propiedades con la notaci\u00f3n de corchetes y no la tradicional notaci\u00f3n de punto. Esto se debe a que para efectos de recorrido, esta notaci\u00f3n puede ser de m\u00e1s utilidad y mejor comprendida en el futuro. Pero esta notaci\u00f3n en Typescript no est\u00e1 permitida, o al menos no lo est\u00e1 hasta que se defina expl\u00edcitamente. Para eso se debe usar la caracter\u00edstica <em><a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/interfaces.html#indexable-types\" target=\"_blank\" rel=\"noreferrer noopener\">index signature<\/a><\/em> en la interfaz del diccionario, la cual veremos a continuaci\u00f3n.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Interfaz de diccionario TranslationDictionaryInterface<\/h3>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-ts\" data-file=\"translator\/TranslationDictionaryInterface.ts\" data-lang=\"TypeScript\"><code>export interface TranslationDictionaryInterface {\n    [key: string]: any;\n\n    helloWorld: string;\n    anotherTransUnit: string;\n    testStringA: string;\n    testStringB: string;\n}\n<\/code><\/pre><\/div>\n\n\n\n<p>La interfaz es bastante sencilla. Su objetivo es el de enumerar las diferentes cadenas de texto traducibles dentro de la aplicaci\u00f3n. Para esto, uso atributos en donde los nombres de los mismos son las claves a usar. Estos atributos son siempre los mismos en todos los diccionarios, por lo que el objetivo se cumple.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><em>Index signature<\/em><\/h4>\n\n\n\n<p>La l\u00ednea 2 es la implementaci\u00f3n de una caracter\u00edstica de Typescript llamada <em>index signature<\/em>. Para el efecto pr\u00e1ctico de esta implementaci\u00f3n, es definir expl\u00edcitamente la forma en como se indexan los atributos, para posteriormente ser accedidos sin errores.<\/p>\n\n\n\n<p>Y todo esto es porque la forma de acceder por medio de clave \u2192 valor, es usar la notaci\u00f3n de corchetes <em>objeto[atributo]<\/em>, en vez de la notaci\u00f3n de punto <em>objeto.atributo<\/em>. Debido a la forma en como Javascript implementa internamente la notaci\u00f3n de corchetes \u2014que es totalmente v\u00e1lida\u2014, hace que el compilador de Typescript no la entienda y arroje un error. Esta l\u00ednea especifica qu\u00e9 tipo de dato es el conjunto clave \u2192 valor, y por lo tanto, el compilador lo sepa y entienda que ese acceso est\u00e1 pensado.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Clase <em>DictionaryInterface<\/em><\/h2>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-ts\" data-file=\"translator\/dictionary\/EsDictionary.ts\" data-lang=\"TypeScript\"><code>import { TranslationDictionaryInterface } from &#39;..\/TranslationDictionaryInterface&#39;\n\nexport default class EsDictionary implements TranslationDictionaryInterface {\n    public helloWorld: string = &#39;\u00a1Hola, mundo!&#39;;\n    public anotherTransUnit: string = &#39;Otra cadena de traducci\u00f3n&#39;;\n    public testStringA: string = &#39;Prueba en espa\u00f1ol A&#39;;\n    public testStringB: string = &#39;Prueba en espa\u00f1ol B&#39;;\n}\n<\/code><\/pre><\/div>\n\n\n\n<p>La implementaci\u00f3n de la interface es sencilla. Esta clase data tiene como objetivo almacenar la carga \u00fatil de todo el servicio. Al implementar la interfaz se asegura que la clase tendr\u00e1 los atributos esperados, lo cual reduce el error humano.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Aplicaci\u00f3n<\/h2>\n\n\n\n<p>Siendo la operaci\u00f3n Translator.trans() est\u00e1tica, solo basta con importarla para que est\u00e9 disponible dentro de todo el contexto, y luego, reemplazar los textos incrustados por las claves de los diccionarios. Tenga en cuenta el siguiente fragmento de c\u00f3digo:<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-ts\" data-lang=\"TypeScript\"><code>import Translator from &#39;.\/translation\/Translator&#39;\n\n\/\/ \u2026\ntry {\n  this.api.setApiKey(currentValue)\n} catch (error: any) {\n  console.error(Translator.trans(&#39;helloWorld&#39;))\n  \/\/ @ts-ignore\n  this.errors.apiKey = [Translator.trans(&#39;anotherTransUnit&#39;)]\n  this.workflowState = 0\n  return\n}\n\/\/ \u2026\n<\/code><\/pre><\/div>\n\n\n\n<p>En las l\u00edneas 7 y 9 se usa el servicio Translator con dos claves previas. En este supuesto caso, si tenemos un servicio API y queremos establecer una clave API inv\u00e1lida, saltar\u00e1 un error e imprimir\u00e1 en consola el texto definido en la clave <em>helloWorld<\/em>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Otras caracter\u00edsticas que podr\u00edan ser implementadas<\/h2>\n\n\n\n<p>Los servicios de traducci\u00f3n tienen otras caracter\u00edsticas \u00fatiles que pueden ser pensadas e implementadas a futuro como:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Traducciones con ordinalidad.<\/li><li>Soporte RTL y LTR.<\/li><li>Extensiones ICU.<\/li><\/ul>\n\n\n\n<p>Sin embargo, tal como est\u00e1 cumple con el objetivo para el cual fue dise\u00f1ado.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Esta aproximaci\u00f3n tiene el objetivo de implementar un servicio estricto de traducci\u00f3n dentro de aplicaciones Javascript y con muy pocos artefactos. Durante todo este a\u00f1o, y para mi sorpresa, fui embarcado en un proyecto donde el 80% del c\u00f3digo estaba escrito en Typescript. Esto aumento mi inter\u00e9s en el lenguaje, ya que ofrece una capa [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[8],"tags":[48,47,46],"class_list":["post-237","post","type-post","status-publish","format-standard","hentry","category-desarrollo-de-software","tag-javascript","tag-traduccion","tag-typescript"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"_links":{"self":[{"href":"https:\/\/www.julianmejio.com\/blog\/wp-json\/wp\/v2\/posts\/237","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.julianmejio.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.julianmejio.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.julianmejio.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.julianmejio.com\/blog\/wp-json\/wp\/v2\/comments?post=237"}],"version-history":[{"count":8,"href":"https:\/\/www.julianmejio.com\/blog\/wp-json\/wp\/v2\/posts\/237\/revisions"}],"predecessor-version":[{"id":250,"href":"https:\/\/www.julianmejio.com\/blog\/wp-json\/wp\/v2\/posts\/237\/revisions\/250"}],"wp:attachment":[{"href":"https:\/\/www.julianmejio.com\/blog\/wp-json\/wp\/v2\/media?parent=237"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.julianmejio.com\/blog\/wp-json\/wp\/v2\/categories?post=237"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.julianmejio.com\/blog\/wp-json\/wp\/v2\/tags?post=237"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}