Desafios da localização no browser e no Node.js

Jota Feldmann
Sep 18, 2018 · 4 min read
“two 10 and 20 Euro banknotes” by Hans Ripa on Unsplash

Imagine a seguinte situação: você precisa converter um valor monetário (“moeda”), de um valor padronizado (“en-us”, “$”) para para um valor localizado (“pt-br”, “R$”).

Pesquisando as últimas formas de realizar isso via JavaScript, e conversando com o time, nesta data (setembro/2018), decidimos utilizar o objeto nativo IntL (internationalization):

new Intl
.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' })
.format(500.00)
// No browser, o retorno é o esperado "R$ 500,00"

Para que outros possam usar o mesmo trecho, componentizei o código, de uma maneira simplificada:

function moneyMask (amount) {
return new Intl
.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' })
.format(amount)
}

Desafio #1: configuração de localização do Jest (e componentes baseados em Node.js)

Com o desafio da localização vencido, passamos para o teste, usando Jest:

expect(moneyMask(6000)).toBe('R$ 6.000,00')
// Temos um erro:
// Expected 'R$ 6.000,00'
// Received 'R$ 6,000.00'

Ao que parece, quando executamos no browser, o resultado é o esperado. No entanto, ao utilizar o Jest, o retorno fica incorreto. É uma mistura de símbolo de moeda do Real, com a divisão gráfica (pontos e vírgulas) do dólar.

Após horas quebrando a cabeça, descobri que o Jest, baseado em Node.js, precisa de configurações para atender o IntL em sua totalidade:

  • Uma das maneiras para que o Node.js atenda às especificações de internacionalização e localização, é utilizar um pacote chamado full-icu;
  • O ICU (International Components for Unicode) é um projeto autônomo, que segundo sua própria descrição:

ICU is a mature, widely used set of C/C++ and Java libraries providing Unicode and Globalization support for software applications. ICU is widely portable and gives applications the same results on all platforms and between C/C++ and Java software.

Solução: instalar e inicializar o ICU completo para qualquer projeto Node.js:

  • Instalando:
npm install full-icu
  • Usando o pacote:
NODE_ICU_DATA=node_modules/full-icu jest --config jest.conf.js

Ao executar um teste simples, agora temos a correção:

expect(new Intl
.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' })
.format(6000.00))
.toBe('R$ 6.000,00')
// Sucesso!
// true

No entando, o teste com a função apresenta nova falha…

Desafio #2: a especificação usa um código, o seu caractere de espaço usa outro

Ao re-executarmos o teste, temos o singular erro:

expect(moneyMask(6000)).toBe('R$ 6.000,00')
// Outro erro:
// Expected 'R$ 6.000,00'
// Received 'R$ 6.000,00'

Mas que diabos está ocorrendo em nosso código? Re-executei o código puro, no browser e temos uma pista:

new Intl
.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' })
.format(6000.00) === 'R$ 6.000,00'
// O mesmo erro:
// false

Aqui a experiência ajudou: visualmente os caracteres são iguais, mas algo na string não está em igualdade. Só pode ser… Algum caractere com codificação diferente: o Unicode. De primeira, testei o espaço. Copiei o espaço do resultado e comparei com o espaço comum, que usamos no teclado:

" " === " "
// false
// O primeiro espaço, do retorno do IntL:
" ".charCodeAt(0)
106
// O segundo espaço, nosso espaço usual, de cada dia, aquele presente no seu belo teclado:
" ".charCodeAt(0)
32

Voilá! Achamos o nosso X. Testando todos os outros caracteres, apenas o espaço tinha um charcode diferente. O nosso caractere espaço de cada dia é o charcode 32, enquanto o charcode do espaço de retorno do objeto IntL é 106.

Isso nos gera um desafio, em termos de teste. O espaço a ser testado é diferente do usual, e a única diferença é do código e não visual.

Solução: converter o código de espaço do retorno de IntL para o espaço usual

O código da função, atualizado em 11/09/2019, com constantes já cacheadas, e um nome melhor para denotar legibilidade do que está acontecendo, ficou assim:

const UNICODE_NON_BREAKING_SPACE = String.fromCharCode(160)
const USUAL_SPACE = String.fromCharCode(32)
const MONEY_MASK = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' })
const fromFloatToMoney = amount => String(MONEY_MASK
.format(amount))
.replace(UNICODE_NON_BREAKING_SPACE, USUAL_SPACE)

E o nosso teste:

expect(fromFloatToMoney(6000)).toBe('R$ 6.000,00')
// Sucesso, enfim:
// true

Resumo e aprendizado

Não basta ter o padrão de uso, é preciso que ele seja fácil de usar no dia-a-dia.

Qual o motivo de usar um caracter que não está presente no teclado? Para cada teste, teria de inserir o caracter de espaço do retorno?

Ainda estou investigando o motivo do espaço charcode 106 ser o utilizado no objeto nativo IntL.

Dúvidas? Sugestões? Comente nesse post.

Agradecimentos

Valeu Ricardo Bin pela dica do pacote full-icu e Willian Tolotti pela dica do IntL e do formato da função.


Topic’s track

Know your body´s made to move, feel it in your guts
Rock’n’roll ain’t worth the name if it don’t make you strut
Don’t sweat it
Get it back to you
Don’t sweat it
Get it back to you
Overkill

“Overkill” — Motörhead

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade