Demonstração de Alternância de Múltiplos Idiomas com JavaScript

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>
    ![](imagens/cont_pt.png)
    ![](imagens/cont_en.png)
    <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

Tags: javascript Internacionalização jQuery i18n l10n

Publicado em 7-31 14:05