Ir para o conteúdo
OneKitly

Formatar números para seis línguas: separadores, moeda e o regresso ao valor

Publicado a 03/10/2025 · 14 min de leitura · Ferramentas de texto e idioma

Daniel Okonkwo

Daniel OkonkwoProgramador front-end e redator de Tecnologia na OneKitly

Desempenho web · Formatos de ficheiro

Verificado a partir de 5 fontes

Ver perfil
Em resumo

A mesma quantidade escreve-se 1 234 567,891 em português europeu, 1.234.567,891 em espanhol, alemão e italiano, e 1,234,567.891 em inglês — e os espaços da versão portuguesa não são espaços que se possam escrever. Ao correr Intl.NumberFormat em Node 26 com ICU 78 aparece U+00A0 NO-BREAK SPACE como separador de grupos do português europeu e U+202F NARROW NO-BREAK SPACE como o do francês, ambos invisíveis e ambos fatais para um analisador que espera um espaço normal ou uma vírgula. Três das seis localizações recusam-se ainda a agrupar números de quatro algarismos: o espanhol, o português europeu e o italiano escrevem 1234 sem separador mas 10 000 com ele, porque o CLDR fixa em dois o seu mínimo de algarismos agrupados. A disposição monetária divide-se do mesmo modo: o inglês põe o símbolo antes dos algarismos sem nada pelo meio, enquanto as outras cinco o põem depois do número com um espaço inquebrável. Nada disto se desfaz com Number.parseFloat, que devolve 1 para a cadeia inglesa e um plausível 1,234 para a alemã. A única análise segura pergunta ao Intl.NumberFormat.formatToParts que caracteres essa localização usa realmente e depois retira o separador de grupos antes de converter o decimal — por essa ordem, porque ao contrário multiplica o valor por mil.

1 234,56 e 1,234.56 são o mesmo número, e confundi-los muda o valor que um leitor lê. Corremos Intl.NumberFormat para as seis localizações do site e imprimimos cada separador — incluindo o invisível que o francês usa — e depois medimos porque parseFloat não consegue desfazer nada disso.

O mesmo número, seis grafias

Pegámos num valor, 1234567.891, e formatámo-lo com Intl.NumberFormat nas seis localizações em que este site publica. O inglês dá 1,234,567.891. O espanhol, o alemão e o italiano dão todos 1.234.567,891 — vírgula e ponto trocaram de papéis, pelo que quem aplique hábitos ingleses lê um número mil vezes menor sem dar por isso. O francês dá 1 234 567,891 e o português europeu também, mas com um carácter invisível diferente a fazer o espaçamento.

Não é um pormenor de apresentação. Uma coluna de folha de cálculo colada de uma convenção para um sistema que espera outra muda em silêncio todos os seus valores, e a falha é invisível porque ambas as grafias são números legais. A regra a interiorizar: um número formatado é um texto acerca de um valor, numa língua concreta, e a língua tem de viajar com ele.

Mais duas convenções merecem ser conhecidas porque aparecem em dados europeus. O alemão da Suíça usa apóstrofo: de-CH formata o mesmo valor como 1'234'567.891, com ponto para o decimal. E uma etiqueta de língua nua não equivale a uma de língua e região: pedir «pt» ao Intl produz convenções brasileiras, 1.234.567,891, enquanto «pt-PT» produz a forma europeia com espaços. Se o seu público lusófono é europeu, a etiqueta nua está silenciosamente errada.

O separador que não se vê

O separador de grupos francês nos dados CLDR atuais não é a barra de espaço. Pedir ao Intl.NumberFormat as partes de um número francês devolve U+202F NARROW NO-BREAK SPACE. O português europeu usa U+00A0 NO-BREAK SPACE, um carácter diferente de largura diferente. Ambos parecem exatamente um espaço em qualquer editor, terminal e navegador, e nenhum deles o é.

As consequências são inteiramente práticas. Uma regra de validação escrita /^[\d ,]+$/ rejeita um número francês corretamente formatado, porque o carácter que ele contém não é o espaço da classe. Um localizar-e-substituir que retira espaços deixa o separador intacto. Uma exportação CSV produz células que a folha de cálculo lê como texto e não como números. E copiar o número de uma página para o colar numa calculadora dá um erro que ninguém sabe explicar, porque o carácter culpado é invisível dos dois lados da colagem.

O correto é nunca adivinhar o separador. Intl.NumberFormat(locale).formatToParts(12345.6) devolve um pequeno arranjo em que uma entrada tem o tipo «group» e outra o tipo «decimal», e os seus valores são exatamente os caracteres que essa localização usa hoje. Leia-os em tempo de execução e o seu código continua a funcionar quando o CLDR mudar, o que acontece: o separador francês era um espaço inquebrável normal em dados mais antigos, antes de o estreito o substituir.

Três localizações não agrupam os números de quatro algarismos

Formate 1234 nas seis localizações e os resultados não batem certo. O inglês dá 1,234, o francês 1 234 e o alemão 1.234 — mas o espanhol, o português europeu e o italiano dão todos 1234, sem separador nenhum. Suba para 10000 e todos agrupam: 10,000, 10 000, 10.000 e 10.000 respetivamente. A mudança ocorre entre os quatro e os cinco algarismos.

A regra por trás é uma definição do CLDR chamada mínimo de algarismos agrupados, que diz quantos algarismos têm de ficar à esquerda do primeiro separador para que agrupar valha a pena. O espanhol, o português europeu e o italiano fixam-na em dois; o inglês, o francês e o alemão em um. Existe porque um número de quatro algarismos nessas línguas é muitas vezes um ano ou uma referência e lê-se melhor sem corte. Se ainda assim precisa do agrupamento — uma tabela de montantes cujas colunas têm de alinhar — passe useGrouping: "always" e o espanhol devolve 1.234. A opção inversa, useGrouping: "min2", faz o inglês comportar-se como o espanhol e imprimir 1234.

Moeda e percentagem: onde fica o símbolo

Para um montante de 1234,50, o inglês formata $1,234.50 — símbolo primeiro, sem espaço, a agrupar desde o milhar. As cinco localizações europeias põem todas o símbolo no fim, e todas um espaço inquebrável antes dele: o francês produz o montante com espaços finos inquebráveis nos algarismos, depois um espaço inquebrável e o símbolo €; o alemão produz 1.234,50 seguido desse espaço e do símbolo; o espanhol, o português e o italiano produzem 1234,50 seguido do mesmo, sem agrupar por causa da regra dos quatro algarismos acima.

Esse espaço inquebrável antes do símbolo é um carácter real e está ali de propósito: impede que uma quebra de linha separe o montante da sua unidade. Também parte os mesmos analisadores ingénuos que o separador de grupos, e faz a comparação de cadeias com um valor esperado escrito à mão falhar nos testes sem razão visível. Compare a saída formatada com formatToParts, não por igualdade com um literal que escreveu.

A percentagem tem a sua própria armadilha, e não é sobre separadores. style: "percent" multiplica por 100 antes de formatar. Passar 12,34 porque já converteu a razão dá 1234% — um número cem vezes maior, impresso sem se queixar. Passe a razão crua, 0,1234, e obtém 12,34%. O espaçamento também difere: o inglês escreve 12.3% sem intervalo, o francês, o espanhol e o alemão inserem um espaço inquebrável antes do sinal, e o português e o italiano não.

Casas decimais, arredondamento e as armadilhas das opções

O valor por omissão para um número simples é um máximo de três casas decimais, pelo que formatar 1,23456 dá 1,235 e o resto desaparece. Para uma moeda o valor por omissão vem da própria moeda: duas casas para o dólar e o euro, zero para o iene, três para o dinar tunisino. É normalmente o que se quer, e vale a pena saber que acontece em vez de supor duas em toda a parte.

As duas opções que geram pedidos de apoio são minimumFractionDigits e maximumFractionDigits. Pôr o mínimo acima do máximo lança um RangeError em vez de cortar, o que pelo menos é ruidoso. Pôr só o mínimo eleva em silêncio o máximo para condizer: minimumFractionDigits: 4 sobre 1,23456789 imprime 1,2346 e não as duas casas que talvez esperasse de outro sítio do código.

O arredondamento tem um valor por omissão que convém conhecer. O Intl arredonda a metade afastando-se de zero: 2,5 passa a 3 e -0,5 passa a -1 com zero casas; passe roundingMode: "halfEven" e 2,5 passa a 2, que é o que a contabilidade e a estatística normalmente querem. E o Intl não é o toFixed: arredondar 1,005 a duas casas dá 1,01 com o Intl e 1,00 com o toFixed, e arredondar 2,675 dá 2,68 com o Intl e 2,67 com o toFixed. A diferença é que o toFixed arredonda o duplo binário, cujo valor fica ligeiríssimamente abaixo do decimal que escreveu, enquanto o Intl arredonda o decimal que pretendia.

A notação compacta, que é uma tradução e não uma abreviatura

Definir notation: "compact" transforma 1234567 em 1.2M em inglês. As outras cinco localizações não usam todas M: o francês dá o montante com um M, o espanhol e o português também, o alemão dá Mio. e o italiano Mln. Na forma longa as diferenças são mais claras: 1.2 million, 1,2 million, 1,2 millones, 1,2 milhões, 1,2 Millionen, 1,2 milioni.

Os milhares são ainda mais díspares. O inglês dá 1.5K para 1500, o francês 1,5 k com k minúsculo, o espanhol e o português 1,5 mil, o italiano 1,5K — e o alemão dá 1500, sem alteração, porque o CLDR não tem forma compacta curta para os milhares em alemão. Se o seu painel assume que todas as localizações encurtam do mesmo modo, a coluna alemã será mais larga do que as outras e não há nada a configurar sobre isso.

Porque parseFloat não consegue desfazer nada disto

toLocaleString é uma função de sentido único. Formatámos 1234567,891 em cada uma das seis localizações e devolvemos o resultado tal e qual ao Number.parseFloat. O inglês devolveu 1. O francês devolveu 1. O português devolveu 1. O espanhol, o alemão e o italiano devolveram 1,234. Nenhum devolveu o valor original, e os três últimos são os perigosos, porque 1,234 é um número perfeitamente plausível que nenhuma validação rejeitará.

O Number() é pelo menos honesto: devolve NaN para as seis, porque nenhuma é um literal numérico válido. Isso faz do Number() a melhor guarda se apenas verifica se uma cadeia é um número de máquina nu, e torna-o inútil como analisador de qualquer coisa que uma pessoa tenha lido.

A ordem da remoção importa mais do que se pensa. Pegue na cadeia alemã 1.234.567,891. Retire primeiro os pontos e depois converta a vírgula em ponto: obtém 1234567.891, correto. Converta primeiro a vírgula em ponto e depois retire os pontos: obtém 1234567891, mil vezes maior e ainda assim um inteiro plausível. São ambas funções de duas linhas e só uma está certa.

Escrevemos a versão guiada pela localização e testámo-la nas seis: ler os caracteres de grupo e decimal a partir de formatToParts, apagar cada ocorrência do carácter de grupo, substituir o carácter decimal por um ponto, deitar fora tudo o que sobrar e não for algarismo nem sinal, e converter. Reproduziu exatamente o valor original nas seis, e também analisou as seis cadeias monetárias, símbolos e espaços inquebráveis incluídos, sem qualquer código específico de localização.

1234567,891
Saída de Intl.NumberFormat para 1234567,891 e para um montante de 1234,50 — Node 26.3.0, ICU 78.3
Localização1234567,891Separador de gruposSeparador decimalMontante, 1234,50
en-US1,234,567.891VírgulaPonto$1,234.50 (símbolo primeiro)
fr-FR1 234 567,891U+202F espaço fino inquebrávelVírgula1 234,50 € (símbolo no fim)
es-ES1.234.567,891PontoVírgula1234,50 € (sem agrupar abaixo de 10 000)
pt-PT1 234 567,891U+00A0 espaço inquebrávelVírgula1234,50 € (sem agrupar abaixo de 10 000)
de-DE1.234.567,891PontoVírgula1.234,50 € (símbolo no fim)
it-IT1.234.567,891PontoVírgula1234,50 € (sem agrupar abaixo de 10 000)
Formatador de númerosAdicione separadores de milhares e defina as casas decimais dos números do texto.Experimentar a ferramenta

Perguntas frequentes

Que separador usa realmente o francês para os milhares?
U+202F NARROW NO-BREAK SPACE, nos dados CLDR em vigor à data — lemo-lo diretamente de formatToParts em Node 26 com ICU 78. Não é a barra de espaço nem U+00A0, que é o que o português europeu usa. Versões antigas do CLDR usavam U+00A0 também para o francês, portanto qualquer código que fixe um deles parte-se numa atualização do motor. Leia o separador em tempo de execução e a pergunta deixa de importar.
Porque é que 1234 se imprime sem separador em espanhol e italiano?
Porque o CLDR fixa em dois o mínimo de algarismos agrupados dessas localizações, o que significa que o agrupamento só começa quando há pelo menos dois algarismos antes do primeiro separador. Assim 1234 escreve-se nu e 10 000 é agrupado. É deliberado: nessas línguas os números de quatro algarismos são muitas vezes anos ou referências. Passe useGrouping: "always" se precisar do separador na mesma, por exemplo para manter uma coluna de números alinhada.
Posso usar toLocaleString e parseFloat como par?
Não, e a falha é silenciosa. Formatámos 1234567,891 em seis localizações e corremos parseFloat sobre cada resultado: três devolveram 1 e três devolveram 1,234. Nenhum devolveu o original. O Number() devolve pelo menos NaN para as seis, portanto falha ruidosamente. Formatar é para mostrar, e analisar precisa do seu próprio caminho de código guiado pelos separadores da localização.
Devo guardar números formatados ou crus?
Crus, sempre, e formate no último momento possível. Um valor guardado 1234567.891 não é ambíguo e a aritmética funciona sobre ele. Uma cadeia guardada 1.234.567,891 carrega uma língua de que depois é preciso lembrar-se, não ordena numericamente e transforma cada cálculo numa análise. A forma formatada pertence à camada de apresentação, gerada a pedido a partir da localização do leitor.
Porque é que toFixed(2) discorda do Intl em 1,005?
Porque arredondam coisas diferentes. O valor em dupla precisão mais próximo de 1,005 é ligeiríssimamente menor do que 1,005, e o toFixed arredonda esse valor binário, produzindo 1,00. O Intl arredonda o número decimal que pediu e produz 1,01. A mesma divisão aparece em 2,675, onde o toFixed dá 2,67 e o Intl dá 2,68. Se houver dinheiro envolvido, use o Intl ou um tipo decimal exato, e nunca misture os dois no mesmo relatório.
Basta um código de língua nu para o Intl?
Nem sempre, e o português é o exemplo mais claro. Pedir «pt» ao Intl dá convenções brasileiras — ponto para os milhares, 1.234.567,891 — porque é aí que está a maioria dos falantes, enquanto «pt-PT» dá a forma europeia com espaço inquebrável. O inglês comporta-se do mesmo modo, ao contrário, nas convenções de data e medida. Se o seu público para uma língua é um país concreto, nomeie o país na etiqueta.

Artigos que podem interessar-lhe

Todos os guias
ExplicaçãoContar palavras é ambíguo, e cada ferramenta responde de forma diferenteUma contagem de palavras é uma definição, não uma medição. Contámos o mesmo parágrafo de quatro maneiras e obtivemos 25, 28, 33 e 38; depois contámos 50 000 caracteres de prosa vulgar e obtivemos acordo dentro de 4,5 %. A diferença deve-se inteiramente a compostos, algarismos e URL.ExplicaçãoOs emoji são mais difíceis do que parecem: porque «basta retirá-los» não tem resposta numa linhaUm emoji visível pode valer um ponto de código ou catorze unidades UTF-16. Corremos três expressões regulares populares sobre uma frase real e cada uma falhou de maneira diferente; uma apagou os algarismos. Eis porquê, que propriedade Unicode responde a que pergunta, e a regra de grupos de grafemas que funciona mesmo.TutorialFiltrar linhas por um padrão sem linha de comandosIsto é o grep para quem não usa grep, com uma diferença importante: a procura é uma subcadeia literal, pelo que uma expressão regular a sério devolve uma caixa vazia e nenhum erro. Cada afirmação foi verificada executando a ferramenta.ExplicaçãoOnde uma linha pode quebrar: o algoritmo Unicode por trás de cada parágrafo ajustado«Quebrar nos espaços» falha na maioria dos sistemas de escrita. O UAX #14 dá a cada caráter uma classe de quebra de linha; procurámos as nossas no Unicode 17.0.0 e executámos uma implementação conforme sobre espaços inquebráveis, hifenes condicionais, espaços de largura zero, URL, japonês e tailandês.GuiaConverter entre formatos de lista sem perder dados: as regras de aspas que ninguém lêPassar de uma lista com quebras de linha para uma lista com vírgulas é trivial até um item conter uma vírgula. As regras de aspas da RFC 4180, porque um campo CSV pode conter uma quebra de linha, porque as folhas de cálculo europeias usam o ponto e vírgula, e o que um item vazio faz à ida e volta — cada caso executado e impresso.GuiaRetirar o Markdown: o que o texto simples perde, e em que se engana uma regexUma ligação torna-se texto com o destino apagado, uma lista aninhada perde a hierarquia, uma tabela torna-se uma fila de palavras. Depois a metade técnica: o markdown não tem uma especificação única, e um limpador à base de regex estraga um nome de ficheiro, um sinal de multiplicação e o interior de um bloco de código — tudo confrontado com um analisador a sério.

Ferramentas relacionadas

Fontes

Detetaste um erro neste artigo?