toLocaleUpperCase() sem locale erra 6 em cada 10 nomes de cidade brasileira
"dia".toLocaleUpperCase(), sem nada entre parênteses, devolve DIA — na sua máquina. Num aparelho configurado em turco, a mesma linha devolve DİA, com o I pontilhado. Não é exemplo de manual: rodamos as 5.571 cidades do Brasil pelas duas réguas, e 60,6% dos nomes saem com pelo menos uma letra diferente — Rio de Janeiro vira RİO DE JANEİRO.
As 27 capitais, pelas duas réguas
Nenhum número desta tabela foi digitado: as duas colunas são capital.toLocaleUpperCase("pt-BR") e capital.toLocaleUpperCase("tr"), rodadas de verdade neste build. 13 das 27 saem diferentes — quase a metade.
| Capital | Maiúscula em pt-BR | Maiúscula em turco |
|---|---|---|
| Rio Branco (AC) | RIO BRANCO | RİO BRANCO |
| Maceió (AL) | MACEIÓ | MACEİÓ |
| Macapá (AP) | MACAPÁ | MACAPÁ |
| Manaus (AM) | MANAUS | MANAUS |
| Salvador (BA) | SALVADOR | SALVADOR |
| Fortaleza (CE) | FORTALEZA | FORTALEZA |
| Brasília (DF) | BRASÍLIA | BRASÍLİA |
| Vitória (ES) | VITÓRIA | VİTÓRİA |
| Goiânia (GO) | GOIÂNIA | GOİÂNİA |
| São Luís (MA) | SÃO LUÍS | SÃO LUÍS |
| Cuiabá (MT) | CUIABÁ | CUİABÁ |
| Campo Grande (MS) | CAMPO GRANDE | CAMPO GRANDE |
| Belo Horizonte (MG) | BELO HORIZONTE | BELO HORİZONTE |
| Belém (PA) | BELÉM | BELÉM |
| João Pessoa (PB) | JOÃO PESSOA | JOÃO PESSOA |
| Curitiba (PR) | CURITIBA | CURİTİBA |
| Recife (PE) | RECIFE | RECİFE |
| Teresina (PI) | TERESINA | TERESİNA |
| Rio de Janeiro (RJ) | RIO DE JANEIRO | RİO DE JANEİRO |
| Natal (RN) | NATAL | NATAL |
| Porto Alegre (RS) | PORTO ALEGRE | PORTO ALEGRE |
| Porto Velho (RO) | PORTO VELHO | PORTO VELHO |
| Boa Vista (RR) | BOA VISTA | BOA VİSTA |
| Florianópolis (SC) | FLORIANÓPOLIS | FLORİANÓPOLİS |
| São Paulo (SP) | SÃO PAULO | SÃO PAULO |
| Aracaju (SE) | ARACAJU | ARACAJU |
| Palmas (TO) | PALMAS | PALMAS |
As capitais que mudam são exatamente as que têm um "i" minúsculo na grafia — nenhuma outra letra do alfabeto se comporta diferente entre os dois locales. Conferido em 13 de agosto de 2026.
A pergunta que ninguém faz: locale de quem?
toLocaleUpperCase() existe para fazer a coisa certa com acento — é o método que a maioria dos tutoriais recomenda no lugar do toUpperCase() cru, porque em teoria ele "sabe o idioma". O que ninguém avisa é: sabe o idioma de QUEM? Não é o idioma da página, não é o lang="pt- BR" do HTML, não é nada que quem escreveu o código decidiu. É o do APARELHO de quem está lendo.
Isso não é comportamento de navegador nem acidente de implementação — está na especificação da linguagem. O ECMA-402, que é o padrão do Intl, lido na origem em tc39.es/ecma402, define toLocaleUpperCase em cima de uma operação chamada TransformCase, e o primeiro passo dela diz: "If locales is undefined, let locales be DefaultLocale()". Chamar sem argumento não é pedir o comportamento neutro — é entregar a decisão para a máquina.
E o que é DefaultLocale()? A própria especificação diz que é "implementation-defined" e devolve "the well-formed and canonicalized language tag for the host environment's current locale" . A nota anexa a essa definição fecha a dúvida para o caso do navegador: "it should match navigator.language" — o idioma que a pessoa escolheu no celular ou no navegador dela, não o que o site pediu.
Por que justamente o turco
O português, o inglês, o espanhol — a imensa maioria dos idiomas do mundo — concordam numa coisa que parece óbvia demais para precisar de regra: a maiúscula de "i" é "I". O turco e o azeri não concordam. As duas línguas têm duas letras onde o alfabeto latino comum só tem uma: um par pontilhado (i minúsculo, İ maiúsculo) e um par sem ponto (ı minúsculo, I maiúsculo). Maiusculizar o "i" do teclado comum, em turco, dá o pontilhado — nunca o "I" que qualquer outro idioma esperaria.
A fonte é o próprio Unicode, lida na origem em unicode.org/Public/UCD/latest/ucd/SpecialCasing.txt, que é o arquivo que todo motor de Intl — V8, ICU, qualquer um — usa para saber quais letras fogem da regra padrão. As linhas, copiadas sem alterar nada:
# When uppercasing, i turns into a dotted capital I
0069; 0069; 0130; 0130; tr; # LATIN SMALL LETTER I
0069; 0069; 0130; 0130; az; # LATIN SMALL LETTER I0069 é o ponto de código do "i" comum; 0130 é "İ", a letra que o Unicode chama de LATIN CAPITAL LETTER I WITH DOT ABOVE. A própria MDN documenta o mesmo achado com o exemplo de sempre dessa área: "istanbul".toLocaleUpperCase("tr") devolve İSTANBUL, nunca ISTANBUL.
A magnitude: 60,6% das cidades brasileiras
Uma letra parece pouco. Uma cidade parece anedota. A pergunta que decide se isso é curiosidade ou defeito de verdade é: quantos nomes reais têm um "i" minúsculo no meio? Rodamos o catálogo inteiro de município do IBGE — o mesmo baixado pelos artigos irmãos de acento, ordem alfabética e duplicata, de servicodados.ibge.gov.br/api/v1/localidades/municipios, baixado em 2026-08-13 — e comparamos toLocaleUpperCase("tr") contra toLocaleUpperCase("pt-BR"), caractere por caractere, nos 5.298 nomes distintos (tirando os 273 que se repetem entre estados, mesma conta do artigo de new Set()).
3.209 de 5.298 — 60,6% — saem com pelo menos uma letra diferente. Não é a cauda rara da distribuição: é a maioria. Qualquer sistema que colete nome de cidade e maiusculize com toLocaleUpperCase() sem locale — etiqueta de correio, recibo, relatório — está numa moeda cara-ou-coroa em que cara é a maioria dos nomes saindo com uma letra errada, tudo porque o celular de quem gerou o documento estava no idioma errado.
O achado mais visual da medição grande não é uma capital — é Piripiri, cidade real do Piauí, com quatro "i": em turco ela sai PİRİPİRİ, com os quatro pontilhados.
Já mordeu gente de verdade — só que não em JavaScript
Este não é um defeito hipotético de linguagem de programação: é uma exceção de idioma que qualquer API de maiúscula sensível a locale herda, em qualquer linguagem. Um post técnico sobre o mesmo problema (Onur Gümüş, "Does Your Code Pass The Turkey Test?", lido em 2026-08-13) descreve um banco cujo sistema em ODP.NET quebrou depois de um deploy: ToUpper(), rodando sob cultura turca, transformava DESCRIPTION em DESCRİPTİON, e uma comparação que esperava a primeira forma parava de bater — uma exceção "cryptic" para quem estava vendo o erro sem saber que a causa era a letra i. A Oracle corrigiu trocando o método por ToUpperInvariant, a versão que ignora o locale do sistema de propósito.
Não é o mesmo motor, não é a mesma linguagem, e este artigo não afirma que o caso do banco aconteceu em JavaScript — seria inventar gravidade que a fonte não tem. O que os dois casos compartilham é a MESMA linha do Unicode citada acima: qualquer API de case-mapping que siga o locale do sistema por padrão pode cair na mesma exceção, porque a exceção não é da API — é da língua turca.
Por que esta ferramenta não erra o mesmo jeito
O conversor de texto desta casa nunca chama toLocaleUpperCase() sem argumento — as quatro chamadas do módulo (em texto.ts) passam "pt-BR" sempre, travado por escrito. Não foi decisão tomada para este artigo: o comentário no topo do arquivo já citava o caso do turco antes de qualquer texto nomear a fonte — a ferramenta sabia do risco antes de virar matéria.
A mesma armadilha, do lado contrário, já está documentada em localizar e substituir: quando a opção "manter caixa" precisa decidir se um termo achado está em maiúscula, ela também passa "pt-BR" explícito pelo mesmo motivo. Ferramenta nenhuma desta casa deixa o Intl escolher sozinho.
Onde isso morde de verdade
Não é preciso um usuário turco para o defeito aparecer — precisa só de um AMBIENTE configurado errado. Uma imagem de contêiner de CI com LANG=tr_TR deixada por engano, um servidor de teste configurado por outra equipe, um desenvolvedor testando internacionalização e esquecendo de trocar de volta — qualquer um desses transforma toda maiusculização sem locale explícito do sistema inteiro, silenciosamente, sem uma linha de erro. O defeito não avisa: o código continua rodando, os testes que não cobrem essa letra continuam verdes, e o texto sai de produção com uma letra errada que ninguém pediu.
O que este artigo não mede
Não medimos nenhum navegador real com o sistema operacional configurado em turco — o ambiente onde este site é escrito não tem essa máquina, e os números vêm do Intl do Node, que usa o mesmo ICU que Chrome e Safari. Não afirmamos quantos usuários turcos visitam sites brasileiros, nem citamos nenhum incidente real em produção de um site em português — o caso do banco, citado acima, é de outra linguagem e está identificado como tal. E não testamos o Lituano, que o Unicode também trata como exceção de case-mapping por um motivo diferente (acento que desaparece depois de certas combinações) — está fora do escopo deste artigo, que é sobre a letra i.