Traducir Aplicaciones Stimulus con I18Next
Spanish (Español) translation by James (you can also view the original English article)
En mi artículo anterior cubierto estímulo — un modesto marco de JavaScript creado por Basecamp. Hoy voy hablar de internacionalización de una aplicación Stimulus, ya que el marco no proporciona ninguna herramienta de I18n fuera de la caja. La internacionalización es un paso importante, especialmente cuando su aplicación es utilizada por personas de todo el mundo, para que una comprensión básica de cómo hacerlo puede ser realmente de utilidad.
Por supuesto, es hasta usted para decidir qué solución de internacionalización a implementar, ya sea jQuery.I18n, Polyglot o algún otro. En este tutorial me gustaría mostrarle un marco I18n popular llamado I18next que tiene un montón de características interesantes y ofrece varios plugins de terceros adicionales para simplificar el proceso de desarrollo aún más. Incluso con todas estas características, I18next no es una herramienta compleja, y no tienes que estudiar un montón de documentación para empezar.
En este artículo, usted aprenderá cómo habilitar el soporte de I18n en las aplicaciones de Stimulus con la ayuda de la biblioteca de I18next. En concreto, vamos a hablar:
- Configuración de I18next
- archivos de traducción y carga de forma asincrónica
- realizar traducciones y traducir la página entera de una sola vez
- trabajar con información de género y plurales
- conmutación entre locales y persistir el escenario solicitado en el parámetro GET
- establecer configuración regional basado en las preferencias del usuario
El código fuente está disponible en el repositorio de GitHub del tutorial.
Arranque la Aplicación Stimulus
Para empezar, vamos a clonar el proyecto arranque de Stimulus e instalar todos las dependencias usando el gestor de paquetes de hilo:
1 |
git clone https://github.com/stimulusjs/stimulus-starter.git |
2 |
cd stimulus-starter
|
3 |
yarn install
|
Vamos a construir una simple aplicación web que se carga la información sobre los usuarios registrados. Para cada usuario, a mostrar su nombre de usuario y el número de fotos que ha subido hasta ahora (no importa lo que estas fotos son).
También, vamos a presentar a un selector de idioma en la parte superior de la página. Cuando se elige un idioma, la interfaz debe traducirse inmediatamente sin recarga de página. Por otra parte, se debe anexar la URL con un ?locale Consiga el parámetro especificar que configuración regional está siendo utilizada actualmente. Por supuesto, si la página está cargada con este parámetro ya, el lenguaje apropiado deberá ajustarse automáticamente.
Bien, vamos a proceder a brindar a nuestros usuarios. Agregue la siguiente línea de código al archivo public/index.html:
1 |
<div data-controller="users" data-users-url="/api/users/index.json"></div> |
Aquí, estamos usando los usuarios control y proporcionar una dirección URL desde la que nuestros usuarios de la carga. En una aplicación del mundo real, probablemente tendríamos un script de servidor que obtiene de los usuarios de la base de datos y responde con JSON. Para este tutorial, sin embargo, vamos a simplemente poner todos los datos necesarios en el archivo public/api/users/index.json:
1 |
[
|
2 |
{
|
3 |
"login": "johndoe", |
4 |
"photos_count": "15", |
5 |
"gender": "male" |
6 |
},
|
7 |
{
|
8 |
"login": "annsmith", |
9 |
"photos_count": "20", |
10 |
"gender": "female" |
11 |
}
|
12 |
]
|
Ahora cree un nuevo archivo src/controllers/users_controller.js:
1 |
import { Controller } from "stimulus" |
2 |
|
3 |
export default class extends Controller { |
4 |
connect() { |
5 |
this.loadUsers() |
6 |
}
|
7 |
}
|
Cuando el controlador está conectado a la DOM, nos estamos cargando asincrónicamente nuestros usuarios con la ayuda del método loadUsers():
1 |
loadUsers() { |
2 |
fetch(this.data.get("url")) |
3 |
.then(response => response.text()) |
4 |
.then(json => { |
5 |
this.renderUsers(json) |
6 |
})
|
7 |
}
|
Este método envía una solicitud de fetch a la URL dada toma la respuesta y finalmente hace que los usuarios:
1 |
renderUsers(users) { |
2 |
let content = '' |
3 |
JSON.parse(users).forEach((user) => { |
4 |
content += `<div>Login: ${user.login}<br>Has uploaded ${user.photos_count} photo(s)</div><hr>` |
5 |
})
|
6 |
this.element.innerHTML = content |
7 |
}
|
renderUsers(), a su vez, analiza JSON, construye una nueva cadena con el contenido y por último muestra este contenido en la página (this.element va a regresar el nodo DOM real que está conectado el controlador, que es el div en nuestro caso).
I18next
Ahora vamos a proceder a la integración de I18next en nuestra aplicación añadir dos bibliotecas a nuestro proyecto: I18next sí mismo y un plugin para habilitar la carga asincrónica de los archivos de traducción de back-end:
1 |
yarn add i18next i18next-xhr-backend |
Vamos a guardar todas las cosas relacionadas con el I18next en un archivo separado del src/i18n/config.js, así que crear ahora:
1 |
import i18next from 'i18next' |
2 |
import I18nXHR from 'i18next-xhr-backend' |
3 |
|
4 |
const i18n = i18next.use(I18nXHR).init({ |
5 |
fallbackLng: 'en', |
6 |
whitelist: ['en', 'ru'], |
7 |
preload: ['en', 'ru'], |
8 |
ns: 'users', |
9 |
defaultNS: 'users', |
10 |
fallbackNS: false, |
11 |
debug: true, |
12 |
backend: { |
13 |
loadPath: '/i18n/{{lng}}/{{ns}}.json', |
14 |
}
|
15 |
}, function(err, t) { |
16 |
if (err) return console.error(err) |
17 |
});
|
18 |
|
19 |
export { i18n as i18n } |
Vamos a ir de arriba hacia abajo para entender lo que está sucediendo aquí:
-
Use(I18nXHR)permite el plugin i18next-xhr-backend.
-
fallbackLngdice que utilizar el inglés como una lengua de la suplencia.
-
whitelistpermite sólo inglés y rusos idiomas para establecerse. Por supuesto, usted puede elegir otros idiomas.
-
preloadmanda archivos de traducción para ser cargado desde el servidor, en lugar de cargarlas cuando se selecciona el idioma correspondiente.
-
nssignifica "namespace" y acepta una cadena o una matriz. En este ejemplo tenemos sólo un espacio de nombres, pero para grandes aplicaciones pueden introducir otros espacios de nombres, comoadmin,cart,profile, etcetera.ns Cada espacio de nombres, debe crearse un archivo de traducción independiente.
-
defaultNSdefine como el espacio de nombres predeterminado para losusuarios.
-
fallbackNSdeshabilita el respaldo de nombres.
-
debugpermite depurar la información que se mostrará en la consola del explorador. Específicamente, dice que traducción se cargan archivos, idioma que es seleccionado, etcetera. Probablemente desee desactivar a esta configuración antes de implementar la aplicación a la producción.
-
backendproporciona la configuración para el plugin I18nXHR y especifica dónde cargar traducciones. Tenga en cuenta que la ruta de acceso debe contener título de la localidad, mientras que el archivo debe ser nombrado después del espacio de nombres y tiene la extensión .json
-
function(err, t)es la devolución de llamada para ejecutar cuando I18next está listo (o cuando un error fue levantado).
A continuación, vamos a crear archivos de traducción. Las traducciones del idioma ruso deben colocarse en el archivo public/i18n/ru/users.json:
1 |
{
|
2 |
"login": "Логин" |
3 |
}
|
entrar aquí es la clave de la traducción, mientras que Логин es el valor para mostrar.
Traducciones al inglés, a su vez, deben ir al archivo public/i18n/en/users.json:
1 |
{
|
2 |
"login": "Login" |
3 |
}
|
Para asegurarse de que funciona de I18next, puede añadir la siguiente línea de código a la devolución de llamada dentro del archivo i18n/config.js:
1 |
// config goes here...
|
2 |
function(err, t) { |
3 |
if (err) return console.error(err) |
4 |
console.log(i18n.t('login')) |
5 |
}
|
Aquí, estamos utilizando un método llamado t que significa "traducir". Este método acepta una clave de la traducción y devuelve el valor correspondiente.
Sin embargo, podemos tener muchas partes de la interfaz de usuario que deben traducirse y hacerlo utilizando el método de t sería bastante tedioso. En cambio, te sugiero que uses otro plugin llamado loc-i18next que permite traducir varios elementos a la vez.
Traducción de Una Sola Vez
Instalar el plugin de loc-i18next:
1 |
yarn add loc-i18next |
Importación en la parte superior del archivo src/i18n/config.js:
1 |
import locI18next from 'loc-i18next' |
Ahora proporcionar la configuración para el plugin de sí mismo:
1 |
// other config
|
2 |
|
3 |
const loci18n = locI18next.init(i18n, { |
4 |
selectorAttr: 'data-i18n', |
5 |
optionsAttr: 'data-i18n-options', |
6 |
useOptionsAttr: true |
7 |
});
|
8 |
|
9 |
export { loci18n as loci18n, i18n as i18n } |
Hay un par de cosas para notar aquí:
-
locI18next.init(i18n)crea una instancia nueva del plugin basado en la instancia previamente definida de I18next. -
selectorAttrespecifica que atributo a usar para detectar elementos que requieren de localización. Básicamente, loc-i18next va a buscar esos elementos y utilizar el valor del atributodata-i18ncomo la clave de la traducción.
-
optionsAttrespecifica que el atributo contiene opciones de traducción adicional.
-
useOptionsAttrindica el plugin para usar las opciones adicionales.
Nuestros usuarios se están cargando, así que tenemos que esperar hasta que esta operación se realiza y sólo realizan localización después de eso. Por ahora, vamos a simplemente establece un contador de tiempo que debe esperar durante dos segundos antes de llamar al método localize(), que es un hack temporal, por supuesto.
1 |
import { loci18n } from '../i18n/config' |
2 |
|
3 |
// other code...
|
4 |
|
5 |
loadUsers() { |
6 |
fetch(this.data.get("url")) |
7 |
.then(response => response.text()) |
8 |
.then(json => { |
9 |
this.renderUsers(json) |
10 |
setTimeout(() => { // <--- |
11 |
this.localize() |
12 |
}, '2000') |
13 |
})
|
14 |
}
|
Código del método localize():
1 |
localize() { |
2 |
loci18n('.users') |
3 |
}
|
Como veis, sólo necesitamos pasar un selector para el plugin de loc-i18next . Todos los elementos de dentro (que tienen el atributo de data-i18n conjunto) se traducirá automáticamente.
Ahora ajustar el método de renderUsers. Por ahora, vamos a traducir sólo la palabra "Login":
1 |
renderUsers(users) { |
2 |
let content = '' |
3 |
JSON.parse(users).forEach((user) => { |
4 |
content += `<div class="users">ID: ${user.id}<br><span data-i18n="login"></span>: ${user.login}<br>Has uploaded ${user.photos_count} photo(s)</div><hr>` |
5 |
})
|
6 |
this.element.innerHTML = content |
7 |
}
|
¡Muy bien! Recargar la página, espere dos segundos y asegúrese de que aparece la palabra "Login" para cada usuario.
Plurales y Género
Hemos localizado parte de la interfaz, que es genial. Aún así, cada usuario tiene dos campos más: el número de fotos subidos y género. Puesto que no podemos predecir cuántas fotos que cada usuario va a tener, la palabra "foto" debe pluralizan correctamente basado en el número dado. Para ello, requerimos un atributo de opciones de data-i18n-options configurado previamente. Para proporcionar la cuenta, se deben asignar opciones de data-i18n-options con el siguiente objeto: {"count": YOUR_COUNT}.
Información de género debe tenerse en cuenta también. La palabra "cargada" en inglés se puede aplicar a hombres y mujeres, pero en ruso se convierte en "загрузил" o "загрузила", por lo que necesitamos data-i18n-options otra vez, que tiene {"context": "GENDER"} como un valor. Por cierto, hay que tener en cuenta que se puede utilizar este contexto para lograr otras tareas, no sólo para proporcionar información de género.
1 |
renderUsers(users) { |
2 |
let content = '' |
3 |
JSON.parse(users).forEach((user) => { |
4 |
content += `<div class="users"><span data-i18n="login"></span>: ${user.login}<br><span data-i18n="uploaded" data-i18n-options="{ 'context': '${user.gender}' }"></span> <span data-i18n="photos" data-i18n-options="{ 'count': ${user.photos_count} }"></span></div><hr>` |
5 |
})
|
6 |
this.element.innerHTML = content |
7 |
}
|
Ahora Actualizar las Traducciones al inglés:
1 |
{
|
2 |
"login": "Login", |
3 |
"uploaded": "Has uploaded", |
4 |
"photos": "one photo", |
5 |
"photos_plural": "{{count}} photos" |
6 |
}
|
Nada complejo aquí. Puesto que el inglés no nos importa la información de género (que es el contexto), la clave de la traducción debe cargarse simplemente. Para proporcionar las traducciones correctamente pluralizadas, estamos usando las teclas photos y photos_plural. La parte de {{count}} es la interpolación y se reemplazará por el número real.
En cuanto a la lengua Rusa, las cosas son más complejas:
1 |
{
|
2 |
"login": "Логин", |
3 |
"uploaded_male": "Загрузил уже", |
4 |
"uploaded_female": "Загрузила уже", |
5 |
"photos_0": "одну фотографию", |
6 |
"photos_1": "{{count}} фотографии", |
7 |
"photos_2": "{{count}} фотографий" |
8 |
}
|
En primer lugar, tenga en cuenta que tenemos las claves uploaded_male y uploaded_female para dos contextos posibles. A continuación, las reglas de pluralización son también más complejas en ruso que en inglés, así que tenemos que ofrecer no dos, sino tres frases posibles. I18next soporta muchos idiomas fuera de la caja, y esta pequeña herramienta puede ayudarle a entender qué teclas de pluralización deben especificarse para un idioma determinado.
Cambiar Configuración Regional
Terminamos con la traducción de nuestra aplicación, pero los usuarios deben ser capaces de cambiar entre configuraciones regionales. Por lo tanto, añadir un nuevo componente "selector de idioma" en el archivo public/index.html:
1 |
<ul data-controller="languages" class="language-switcher"></ul> |
Arte el controlador correspondiente dentro del archivo src/controllers/languages_controller.js:
1 |
import { Controller } from "stimulus" |
2 |
import { i18n, loci18n } from '../i18n/config' |
3 |
|
4 |
export default class extends Controller { |
5 |
initialize() { |
6 |
let languages = [ |
7 |
{title: 'English', code: 'en'}, |
8 |
{title: 'Русский', code: 'ru'} |
9 |
]
|
10 |
|
11 |
this.element.innerHTML = languages.map((lang) => { |
12 |
return `<li data-action="click->languages#switchLanguage" |
13 |
data-lang="${lang.code}">${lang.title}</li>` |
14 |
}).join('') |
15 |
}
|
16 |
}
|
Aquí estamos utilizando el callback initialize() para mostrar una lista de idiomas soportados. Cada li tiene un atributo de data-action que especifica qué método (switchLanguage, en este caso) debe activarse al hacer clic en el elemento.
Ahora agregue el método switchLanguage():
1 |
switchLanguage(e) { |
2 |
this.currentLang = e.target.getAttribute("data-lang") |
3 |
}
|
Simplemente toma el objetivo del evento y toma el valor del atributo data-lang.
También me gustaría añadir un getter y setter para el atributo de currentLang:
1 |
get currentLang() { |
2 |
return this.data.get("currentLang") |
3 |
}
|
4 |
|
5 |
set currentLang(lang) { |
6 |
if(i18n.language !== lang) { |
7 |
i18n.changeLanguage(lang) |
8 |
}
|
9 |
|
10 |
if(this.currentLang !== lang) { |
11 |
this.data.set("currentLang", lang) |
12 |
loci18n('body') |
13 |
this.highlightCurrentLang() |
14 |
}
|
15 |
}
|
El comprador es muy simple, obtener el valor de la lengua usada actualmente y devolverlo.
El setter es más complejo. En primer lugar, utilizamos el método changeLanguage si el idioma actualmente establecido no es igual al seleccionado. También, estamos almacenando la configuración regional nuevamente seleccionada bajo el atributo de data-current-lang (que se accede en el getter), localizar el cuerpo de la página HTML utilizando el plugin loc-i18next y por último destacar la configuración regional utiliza actualmente.
¡Código de la highlightCurrentLang():
1 |
highlightCurrentLang() { |
2 |
this.switcherTargets.forEach((el, i) => { |
3 |
el.classList.toggle("current", this.currentLang === el.getAttribute("data-lang")) |
4 |
})
|
5 |
}
|
Aquí estamos iterando sobre una matriz de conmutadores de configuración regional y de comparar los valores de sus atributos de data-lang en el valor de la configuración regional utiliza actualmente. Si los valores coinciden, el conmutador se le asigna una clase CSS actual, de lo contrario se quita esta clase.
Para hacer la this.switcherTargets construir trabajo, debemos definir las metas del estímulo de la siguiente manera:
1 |
static targets = [ "switcher" ] |
También, agregar atributos de data-target con valores del switcher de li:
1 |
initialize() { |
2 |
// ...
|
3 |
this.element.innerHTML = languages.map((lang) => { |
4 |
return `<li data-action="click->languages#switchLanguage" |
5 |
data-target="languages.switcher" data-lang="${lang.code}">${lang.title}</li>` |
6 |
}).join('') |
7 |
// ...
|
8 |
}
|
Otra cosa importante a considerar es que archivos de traducción pueden tomar algún tiempo para cargar, y tenemos que esperar a esta operación completar antes de permitir que la configuración regional que se cambiará. Por lo tanto, vamos a aprovechar el callback cargado:
1 |
initialize() { |
2 |
i18n.on('loaded', (loaded) => { // <--- |
3 |
let languages = [ |
4 |
{title: 'English', code: 'en'}, |
5 |
{title: 'Русский', code: 'ru'} |
6 |
]
|
7 |
|
8 |
this.element.innerHTML = languages.map((lang) => { |
9 |
return `<li data-action="click->languages#switchLanguage" |
10 |
data-target="languages.switcher" data-lang="${lang.code}">${lang.title}</li>` |
11 |
}).join('') |
12 |
|
13 |
this.currentLang = i18n.language |
14 |
})
|
15 |
}
|
Por último, no olvides quitar setTimeout desde el método loadUsers():
1 |
loadUsers() { |
2 |
fetch(this.data.get("url")) |
3 |
.then(response => response.text()) |
4 |
.then(json => { |
5 |
this.renderUsers(json) |
6 |
this.localize() |
7 |
})
|
8 |
}
|
Configuración Regional que Persiste en la URL
Después de la configuración regional esté, me gustaría añadir una ?lang consigue parámetro a la URL que contiene el código del idioma elegido. Añadiendo un parámetro GET sin necesidad de recargar la página se puede hacer fácilmente con la ayuda de la Historia de la API:
1 |
set currentLang(lang) { |
2 |
if(i18n.language !== lang) { |
3 |
i18n.changeLanguage(lang) |
4 |
window.history.pushState(null, null, `?lang=${lang}`) // <--- |
5 |
}
|
6 |
|
7 |
if(this.currentLang !== lang) { |
8 |
this.data.set("currentLang", lang) |
9 |
loci18n('body') |
10 |
this.highlightCurrentLang() |
11 |
}
|
12 |
}
|
Detecta Locale
Lo último que vamos a implementar hoy en día es la capacidad para establecer la configuración regional basada en las preferencias del usuario. Un plugin llamado LanguageDetector nos puede ayudar a resolver esta tarea. Agregar un nuevo paquete de hilado:
1 |
yarn add i18next-browser-languagedetector |
LanguageDetector de importación dentro del archivo i18n/config.js:
1 |
import LngDetector from 'i18next-browser-languagedetector' |
Ahora modificar la configuración:
1 |
const i18n = i18next.use(I18nXHR).use(LngDetector).init({ // <--- |
2 |
// other options go here...
|
3 |
detection: { |
4 |
order: ['querystring', 'navigator', 'htmlTag'], |
5 |
lookupQuerystring: 'lang', |
6 |
}
|
7 |
}, function(err, t) { |
8 |
if (err) return console.error(err) |
9 |
});
|
La opción orden enumera todas las técnicas (clasificadas por su importancia) que trate del plugin para "adivinar" el escenario preferido:
-
QueryStringsignifica comprobar un param GET que contiene código de la localidad. -
lookupQuerystringestablece el nombre del parámetro GET a utilizar, que eslangen nuestro caso. -
navigatorsignifica obtener datos de configuración de la solicitud del usuario. -
htmlTagconsiste en traer el escenario preferido del atributolangde la etiqueta dehtml.
Conclusión
En este artículo hemos dado un vistazo a I18next, una solución popular para traducir aplicaciones JavaScript con facilidad. Han aprendido cómo integrar I18next con el marco Stimulus, configurarlo y cargar archivos de traducción de manera asincrónica. También, has visto cómo cambiar entre configuraciones regionales y establecer el idioma predeterminado basado en las preferencias del usuario.
I18next tiene algunas opciones de configuración adicionales y muchos plugins, así que asegúrese de examinar su documentación oficial para aprender más. También nota que estímulo no te obligan a utilizar una solución de localización específica, por lo que también puede tratar de usar algo como jQuery.I18n o Polyglot.
Eso es todo por hoy! Gracias por la lectura a lo largo y hasta la próxima vez.



