Para atender às necessiddaes de internacionalização de websites, é comum o uso de arquivos de idioma. Neste artigo, vamos explorar como implementar um sistema de alternância de idiomas utilizando JavaScript, com auxílio do plugin jquery.i18n.properteis.
A implementação requer a inclusão da biblioteca jQuery, que serve como base para o funcionamennto do plugin.
Estrutura do Projeto
Código Fonte Vamos analisar a implementação detalhada do sistema de internacionalização:
<html lang="pt">
<head>
<meta charset="utf-8" />
<title>Teste de Internacionalização com JavaScript</title>
<style>
.portugues .flag_pt{
display:block;
}
.portugues .flag_en{
display:none;
}
.ingles .flag_pt{
display:none;
}
.ingles .flag_en{
display:block;
}
.background_icon{
background-position:0 0;
width:30px;
height:30px;
}
.portugues .background_icon{
background-image:url('imagens/flag_pt.png');
}
.ingles .background_icon{
background-image:url('imagens/flag_en.png');
}
</style>
</head>
<body class="portugues">
<div class="container">
<h1 data-i18n="titulo_principal">Título Principal</h1>
<p data-i18n="conteudo_paragrafo">Conteúdo do parágrafo, informações relevantes aqui</p>
<nav class="menu_principal">
<li><a data-i18n="menu_item1">Item 1</a></li>
<li><a data-i18n="menu_item2">Item 2</a></li>
<li><a data-i18n="menu_item3">Item 3</a></li>
<li><a data-i18n="menu_item4">Item 4</a></li>
</nav>


<section class="secao_um">
<div class="interno">
<h2 data-i18n="secao_um_titulo">Primeira Seção</h2>
<p data-i18n="secao_um_conteudo">Contúdo da primeira seção do site</p>
</div>
</section>
<div class="background_icon"></div>
</div>
<button id="alternar_idioma" data-i18n="botao_alterar">Mudar para Inglês</button>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="js/jquery.i18n.properties-1.0.9.js"></script>
<script>
const gerenciadorIdiomas = {
idiomaAtual : 0, // 0:pt,1:en
carregarPropriedades : function(novoIdioma){
const instancia = this;
const idiomaTemp = '';
if(novoIdioma === 0){
idiomaTemp = 'portugues';
$('body').removeClass('ingles').addClass('portugues');
}else{
idiomaTemp = 'ingles';
$('body').removeClass('portugues').addClass('ingles');
}
jQuery.i18n.properties({
name: 'traducoes',
path:'idiomas/',
language: idiomaTemp,
cache: false,
mode:'map',
callback: function() {
Object.keys($.i18n.map).forEach(chave => {
$(`[data-i18n="${chave}"]`).text($.i18n.map[chave]);
});
document.title = $.i18n.map['titulo_pagina'];
}
});
instancia.idiomaAtual = novoIdioma;
}
}
$(document).ready(function(){
gerenciadorIdiomas.carregarPropriedades(0);
$('#alternar_idioma').on('click', function(){
const proximoIdioma = gerenciadorIdiomas.idiomaAtual === 0 ? 1 : 0;
gerenciadorIdiomas.carregarPropriedades(proximoIdioma);
});
});
</script>
</body>
</html>
Visualizar CódigoArquivo de idioma português: traducoes_pt.properties
titulo_pagina=Teste de Internacionalização com JavaScript
titulo_principal=Título Principal
conteudo_paragrafo=Conteúdo do parágrafo, informações relevantes aqui
menu_item1=Item 1
menu_item2=Item 2
menu_item3=Item 3
menu_item4=Item 4
secao_um_titulo=Primeira Seção
secao_um_conteudo=Contúdo da primeira seção do site
botao_alterar=Mudar para Inglês
Arquivo de idioma inglês: traducoes_en.properties
titulo_pagina=JavaScript Internationalization Test
titulo_principal=Main Title
conteudo_paragrafo=Paragraph content, relevant information here
menu_item1=Item 1
menu_item2=Item 2
menu_item3=Item 3
menu_item4=Item 4
secao_um_titulo=First Section
secao_um_conteudo=Content of the first section
botao_alterar=Switch to Portuguese
/******************************************************************************
* jquery.i18n.properties
*
* Licenciado sob as licenças GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) e
* MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt).
*
* @version 1.0.x
* @autor Nuno Fernandes
* @url www.codingwithcoffee.com
* @inspiração Localização para jQuery (http://keith-wood.name/localisation.html)
* por Keith Wood (kbwood{at}iinet.com.au) Junho 2007
*
*****************************************************************************/
(function($) {
$.i18n = {};
/** Mapa contendo chaves de bundles (se modo: 'map') */
$.i18n.map = {};
/**
* Carrega e analisa arquivos de bundle de mensagens (.properties),
* tornando as chaves disponíveis como variáveis JavaScript.
*
* Os arquivos i18n são nomeados <nome>.js, ou <nome>_<idioma>.js ou <nome>_<idioma>_<pais>.js
* Onde:
* O argumento <idioma> é um Código de Idioma ISO válido. Estes códigos são
* os códigos de duas letras minúsculas, conforme definido por ISO-639.
* O argumento <pais> é um Código de País ISO válido. Estes códigos são
* os códigos de duas letras maiúsculas, conforme definido por ISO-3166.
*
* Exemplo de uso para um bundle/Mensagens.properties:
* $.i18n.properties({
* name: 'Mensagens',
* language: 'en_US',
* path: 'bundles'
* });
* @param nome (string/string[], opcional) nomes dos arquivos para carregar (ex: 'Mensagens' ou ['Msg1','Msg2']). Padrão: "Mensagens"
* @param language (string, opcional) código de idioma/país (ex: 'pt', 'pt_BR', 'en_US'). Se não especificado, o idioma relatado pelo navegador será usado.
* @param path (string, opcional) caminho do diretório que contém o arquivo para carregar
* @param mode (string, opcional) se as chaves dos bundles estão disponíveis como variáveis/funções JavaScript ou como um mapa (ex: 'vars' ou 'map')
* @param cache (boolean, opcional) se os bundles devem ser cacheados pelo navegador ou recarregados em cada carregamento de página. Padrão: false
* @param encoding (string, opcional) a codificação a ser solicitada para os bundles. Arquivos de propriedade de resource bundles são especificados para estar no formato ISO-8859-1. Padrão: UTF-8
* @param callback (function, opcional) função de callback a ser chamada após o script ser terminado
*/
$.i18n.properties = function(configuracoes) {
// configurando padrões
const padroes = {
nome: 'Mensagens',
language: '',
path: '',
mode: 'vars',
cache: false,
encoding: 'UTF-8',
callback: null
};
configuracoes = $.extend(padroes, configuracoes);
if(configuracoes.language === null || configuracoes.language === '') {
configuracoes.language = $.i18n.browserLang();
}
if(configuracoes.language === null) {configuracoes.language='';}
// carrega e analisa arquivos de bundle
const arquivos = obterArquivos(configuracoes.nome);
for(let i=0; i<arquivos.length; i++) {
// 1. carrega base (ex: Mensagens.properties)
loadAndParseFile(configuracoes.path + arquivos[i] + '.properties', configuracoes);
// 2. com código de idioma (ex: Mensagens_pt.properties)
if(configuracoes.language.length >= 2) {
loadAndParseFile(configuracoes.path + arquivos[i] + '_' + configuracoes.language.substring(0, 2) +'.properties', configuracoes);
}
// 3. com código de idioma e país (ex: Mensagens_pt_BR.properties)
if(configuracoes.language.length >= 5) {
loadAndParseFile(configuracoes.path + arquivos[i] + '_' + configuracoes.language.substring(0, 5) +'.properties', configuracoes);
}
}
// executa callback
if(configuracoes.callback){ configuracoes.callback(); }
};
/**
* Quando configurado com modo: 'map', permite acesso aos valores do bundle especificando sua chave.
* Ex: jQuery.i18n.prop('empresa.bundles.menu_adicionar')
*/
$.i18n.prop = function(chave /* Adicione parâmetros como argumentos da função conforme necessário */) {
let valor = $.i18n.map[chave];
if (valor == null)
return '[' + chave + ']';
// Substituição de placeholders
let i;
if (typeof(valor) == 'string') {
// Manipula caracteres de escape. Feito separadamente do loop de tokenização abaixo porque caracteres de escape são
// ativos em strings entre aspas.
i = 0;
while ((i = valor.indexOf('\\', i)) != -1) {
if (valor[i+1] == 't')
valor = valor.substring(0, i) + '\t' + valor.substring((i++) + 2); // tab
else if (valor[i+1] == 'r')
valor = valor.substring(0, i) + '\r' + valor.substring((i++) + 2); // return
else if (valor[i+1] == 'n')
valor = valor.substring(0, i) + '\n' + valor.substring((i++) + 2); // line feed
else if (valor[i+1] == 'f')
valor = valor.substring(0, i) + '\f' + valor.substring((i++) + 2); // form feed
else if (valor[i+1] == '\\')
valor = valor.substring(0, i) + '\\' + valor.substring((i++) + 2); // \
else
valor = valor.substring(0, i) + valor.substring(i+1); // Descarta silenciosamente o caractere
}
// Converte a string em uma lista de tokens.
const arr = [], j, indice;
i = 0;
while (i < valor.length) {
if (valor[i] == '\'') {
// Manipula aspas
if (i == valor.length-1)
valor = valor.substring(0, i); // Descarta silenciosamente a aspa final
else if (valor[i+1] == '\'')
valor = valor.substring(0, i) + valor.substring(++i); // Aspa escapada
else {
// String entre aspas
j = i + 2;
while ((j = valor.indexOf('\'', j)) != -1) {
if (j == valor.length-1 || valor[j+1] != '\'') {
// Encontrou início e fim das aspas. Remove-as
valor = valor.substring(0,i) + valor.substring(i+1, j) + valor.substring(j+1);
i = j - 1;
break;
}
else {
// Encontrou uma aspa dupla, reduz para uma única.
valor = valor.substring(0,j) + valor.substring(++j);
}
}
if (j == -1) {
// Não há aspa final. Remove a aspa inicial
valor = valor.substring(0,i) + valor.substring(i+1);
}
}
}
else if (valor[i] == '{') {
// Início de um placeholder não entre aspas.
j = valor.indexOf('}', i+1);
if (j == -1)
i++; // Não há fim. Processa o resto da linha. Java lançaria uma exceção
else {
// Adiciona 1 ao índice para alinhar com os argumentos da função.
indice = parseInt(valor.substring(i+1, j));
if (!isNaN(indice) && indice >= 0) {
// Coloca a linha até agora (se não estiver vazia) no array
const s = valor.substring(0, i);
if (s != "")
arr.push(s);
// Coloca a referência do parâmetro no array
arr.push(indice);
// Reinicia o processamento a partir do resto da linha.
i = 0;
valor = valor.substring(j+1);
}
else
i = j + 1; // Parâmetro inválido. Deixa como está.
}
}
else
i++;
}
// Coloca o resto da linha não vazia no array.
if (valor != "")
arr.push(valor);
valor = arr;
// Torna o array o valor para a entrada.
$.i18n.map[chave] = arr;
}
if (valor.length == 0)
return "";
if (valor.length == 1 && typeof(valor[0]) == "string")
return valor[0];
let s = "";
for (i=0; i<valor.length; i++) {
if (typeof(valor[i]) == "string")
s += valor[i];
// Deve ser um número
else if (valor[i] + 1 < arguments.length)
s += arguments[valor[i] + 1];
else
s += "{"+ valor[i] +"}";
}
return s;
};
/** Idioma relatado pelo navegador, código normalizado */
$.i18n.browserLang = function() {
return normalizarCodigoIdioma(navigator.language /* Mozilla */ || navigator.userLanguage /* IE */);
}
/** Carrega e analisa arquivos .properties */
function loadAndParseFile(nomeArquivo, configuracoes) {
$.ajax({
url: nomeArquivo,
async: false,
cache: configuracoes.cache,
contentType:'text/plain;charset='+ configuracoes.encoding,
dataType: 'text',
success: function(dados, status) {
parseData(dados, configuracoes.mode);
}
});
}
/** Analisa arquivos .properties */
function parseData(dados, modo) {
let analisado = '';
const parametros = dados.split( /\n/ );
const regPlaceholder = /(\{\d+\})/g;
const regRepPlaceholder = /\{(\d+)\}/g;
const unicodeRE = /(\\u.{4})/ig;
for(let i=0; i<parametros.length; i++ ) {
parametros[i] = parametros[i].replace( /^\s\s*/, '' ).replace( /\s\s*$/, '' ); // trim
if(parametros[i].length > 0 && parametros[i].match("^#")!="#") { // pula comentários
const par = parametros[i].split('=');
if(par.length > 0) {
/** Processa chave & valor */
const nome = unescape(par[0]).replace( /^\s\s*/, '' ).replace( /\s\s*$/, '' ); // trim
let valor = par.length == 1 ? "" : par[1];
// processa valores de múltiplas linhas
while(valor.match(/\\$/)=="\\") {
valor = valor.substring(0, valor.length - 1);
valor += parametros[++i].replace( /\s\s*$/, '' ); // right trim
}
// Coloca valores com '='s embutidos de volta juntos
for(let s=2;s<par.length;s++){ valor +='=' + par[s]; }
valor = valor.replace( /^\s\s*/, '' ).replace( /\s\s*$/, '' ); // trim
/** Modo: chaves de bundle em um mapa */
if(modo == 'map' || modo == 'both') {
// lida com chars unicode possivelmente deixados de fora
const unicodeMatches = valor.match(unicodeRE);
if(unicodeMatches) {
for(let u=0; u<unicodeMatches.length; u++) {
valor = valor.replace( unicodeMatches[u], unescapeUnicode(unicodeMatches[u]));
}
}
// adiciona ao mapa
$.i18n.map[nome] = valor;
}
/** Modo: chaves de bundle como vars/funções */
if(modo == 'vars' || modo == 'both') {
valor = valor.replace( /"/g, '\\"' ); // escape aspas (")
// garante que o namespace da chave existe (ex, 'alguma.chave')
verificarNamespaceChave(nome);
// valor com substituições de variáveis
if(regPlaceholder.test(valor)) {
const partes = valor.split(regPlaceholder);
// processa args da função
let primeiro = true;
let fnArgs = '';
const usedArgs = [];
for(let p=0; p<partes.length; p++) {
if(regPlaceholder.test(partes[p]) && (usedArgs.length == 0 || usedArgs.indexOf(partes[p]) == -1)) {
if(!primeiro) {fnArgs += ',';}
fnArgs += partes[p].replace(regRepPlaceholder, 'v$1');
usedArgs.push(partes[p]);
primeiro = false;
}
}
analisado += nome + '=function(' + fnArgs + '){';
// processa corpo da função
const fnExpr = '"' + valor.replace(regRepPlaceholder, '"+v$1+"') + '"';
analisado += 'return ' + fnExpr + ';' + '};';
// valor simples
}else{
analisado += nome+'="'+valor+'";';
}
} // FIM: Modo: chaves de bundle como vars/funções
} // FIM: if(par.length > 0)
} // FIM: pula comentários
}
eval(analisado);
}
/** Garante que o namespace existe (para chaves com pontos no nome) */
function verificarNamespaceChave(chave) {
const regPonto = /\./;
if(regPonto.test(chave)) {
let fullname = '';
const nomes = chave.split( /\./ );
for(let i=0; i<nomes.length; i++) {
if(i>0) {fullname += '.';}
fullname += nomes[i];
if(eval('typeof '+fullname+' == "undefined"')) {
eval(fullname + '={};');
}
}
}
}
/** Garante que o nome do arquivo é um array */
function obterArquivos(nomes) {
return (nomes && nomes.constructor == Array) ? nomes : [nomes];
}
/** Garante que o código do idioma está no formato aa_AA. */
function normalizarCodigoIdioma(lang) {
lang = lang.toLowerCase();
if(lang.length > 3) {
lang = lang.substring(0, 3) + lang.substring(3).toUpperCase();
}
return lang;
}
/** Desfaz chars unicode ('\u00e3') */
function unescapeUnicode(str) {
// unescape unicode codes
const codes = [];
const code = parseInt(str.substr(2), 16);
if (code >= 0 && code < Math.pow(2, 16)) {
codes.push(code);
}
// convert codes to text
let unescaped = '';
for (let i = 0; i < codes.length; ++i) {
unescaped += String.fromCharCode(codes[i]);
}
return unescaped;
}
/* Cross-Browser Split 1.0.1
(c) Steven Levithan <stevenlevithan.com>; MIT License
An ECMA-compliant, uniform cross-browser split method */
let cbSplit;
// avoid running twice, which would break `cbSplit._nativeSplit`'s reference to the native `split`
if (!cbSplit) {
cbSplit = function(str, separator, limit) {
// if `separator` is not a regex, use the native `split`
if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
if(typeof cbSplit._nativeSplit == "undefined")
return str.split(separator, limit);
else
return cbSplit._nativeSplit.call(str, separator, limit);
}
const output = [],
lastLastIndex = 0,
flags = (separator.ignoreCase ? "i" : "") +
(separator.multiline ? "m" : "") +
(separator.sticky ? "y" : ""),
separator = RegExp(separator.source, flags + "g"), // make `global` and avoid `lastIndex` issues by working with a copy
separator2, match, lastIndex, lastLength;
str = str + ""; // type conversion
if (!cbSplit._compliantExecNpcg) {
separator2 = RegExp("^" + separator.source + "$(?!\\s)", flags); // doesn't need /g or /y, but they don't hurt
}
/* behavior for `limit`: if it's...
- `undefined`: no limit.
- `NaN` or zero: return an empty array.
- a positive number: use `Math.floor(limit)`.
- a negative number: no limit.
- other: type-convert, then use the above rules. */
if (limit === undefined || +limit < 0) {
limit = Infinity;
} else {
limit = Math.floor(+limit);
if (!limit) {
return [];
}
}
while (match = separator.exec(str)) {
lastIndex = match.index + match[0].length; // `separator.lastIndex` is not reliable cross-browser
if (lastIndex > lastLastIndex) {
output.push(str.slice(lastLastIndex, match.index));
// fix browsers whose `exec` methods don't consistently return `undefined` for nonparticipating capturing groups
if (!cbSplit._compliantExecNpcg && match.length > 1) {
match[0].replace(separator2, function () {
for (let i = 1; i < arguments.length - 2; i++) {
if (arguments[i] === undefined) {
match[i] = undefined;
}
}
});
}
if (match.length > 1 && match.index < str.length) {
Array.prototype.push.apply(output, match.slice(1));
}
lastLength = match[0].length;
lastLastIndex = lastIndex;
if (output.length >= limit) {
break;
}
}
if (separator.lastIndex === match.index) {
separator.lastIndex++; // avoid an infinite loop
}
}
if (lastLastIndex === str.length) {
if (lastLength || !separator.test("")) {
output.push("");
}
} else {
output.push(str.slice(lastLastIndex));
}
return output.length > limit ? output.slice(0, limit) : output;
};
cbSplit._compliantExecNpcg = /()??/.exec("")[1] === undefined; // NPCG: nonparticipating capturing group
cbSplit._nativeSplit = String.prototype.split;
} // end `if (!cbSplit)`
String.prototype.split = function (separator, limit) {
return cbSplit(this, separator, limit);
};
})(jQuery);
jquery.i18n.properties-1.0.9.js