Bandeiras em widgets: por que códigos ISO de país superam nomes localizados
O bug era sutil. Um widget iOS exibia o nome do país correto em inglês, mas mostrava o emoji de bandeira errado em alemão. Na localidade francesa, um país diferente mostrava a bandeira errada. As bandeiras não eram aleatórias — eram consistentemente erradas em um padrão que levou um momento para reconhecer: a busca estava retornando a bandeira de qualquer país que, na localidade atual, tivesse um nome que se assemelhasse ao nome de exibição armazenado.
A causa raiz era uma busca de países que usava nomes de países localizados como chaves de dicionário. Nomes que são strings estáveis em uma localidade se tornam strings diferentes em outra. Corrigi-lo exigiu reconstruir a busca em torno de códigos ISO 3166-1 alpha-2 por toda parte, e a lição vai muito além de emojis de bandeira.
Como o bug apareceu
O widget exibia uma lista de países com seus emojis de bandeira correspondentes ao lado de outros dados. A implementação original armazenava os nomes de exibição dos países como identificador — a string “Germany” ou “France” — e usava um dicionário com chaves nesses nomes para buscar o emoji de bandeira correspondente.
Na localidade inglesa, isso funcionava corretamente. O nome armazenado era “Germany”, a chave do dicionário era “Germany”, e a busca retornava a bandeira correta. Mas quando a localidade do dispositivo era definida como alemão, NSLocale.current.localizedString(forRegionCode: "DE") retorna “Deutschland”. O widget estava chamando esse método para resolver os nomes de exibição dinamicamente. Então o nome exibido se tornava “Deutschland” — correto, em contexto — mas a busca no dicionário com chaves em inglês falhava, recorria a um valor padrão e exibia a bandeira errada.
A variante mais insidiosa eram correspondências parciais. Em algumas localidades, o nome localizado de um país coincide com um prefixo ou correspondência aproximada do nome em inglês de um país diferente. Sem verificações de igualdade estrita, buscas aproximadas retornavam resultados plausíveis, mas incorretos. Um processo de controle de qualidade executado apenas na localidade inglesa nunca detectaria isso.
Por que nomes dependentes de localidade são uma armadilha
O problema fundamental é usar uma string dependente de localidade como identificador. Identificadores precisam ser estáveis — o mesmo valor independentemente de quem os lê, quando, ou em que contexto de idioma. O nome de um país na localidade atual é um valor de apresentação. É correto e apropriado exibi-lo para um usuário. É incorreto e frágil usá-lo como chave de busca, identificador de banco de dados, chave de cache ou qualquer outra forma de referência estável.
Isso não se limita a nomes de países. Nomes de moedas, nomes de idiomas, nomes de exibição de localidade e descrições de fuso horário compartilham a mesma propriedade: são strings dependentes de localidade que o sistema gera para exibição, não para identidade. NSLocale.current.localizedString(forCurrencyCode: "EUR") retorna “Euro” em inglês e “Euro” em alemão, o que por acaso coincide — mas localizedString(forCurrencyCode: "GBP") retorna “British Pound” em inglês e “Britisches Pfund” em alemão. Usar qualquer um como chave produz a mesma categoria de bug.
A regra é simples: use o código ISO como identificador em toda a lógica e chame o método de localização apenas ao construir uma string para exibição ao usuário. Nunca armazene a string localizada. Nunca a use como chave. Nunca a compare programaticamente entre localidades.
Gerando emojis de bandeira a partir de códigos ISO
Os emojis de bandeira são codificados no Unicode como pares de símbolos indicadores regionais. A sequência de caracteres para a bandeira de um determinado país corresponde ao código ISO 3166-1 alpha-2 do país, com cada letra substituída pelo seu símbolo indicador regional equivalente. Os símbolos indicadores regionais vão de U+1F1E6 (para A) até U+1F1FF (para Z).
Em Swift, gerar um emoji de bandeira a partir de um código ISO é uma função utilitária curta:
func flagEmoji(for isoCode: String) -> String {
let base: UInt32 = 0x1F1E6 - 65 // deslocamento a partir de A
return isoCode.uppercased().unicodeScalars.compactMap {
UnicodeScalar(base + $0.value)
}.map { String($0) }.joined()
}
Essa função recebe “DE” e retorna 🇩🇪. Recebe “SG” e retorna 🇸🇬. Não requer dicionário, arquivo de mapeamento ou manutenção. O mapeamento é definido pelo padrão Unicode e corresponde aos códigos ISO 3166-1 alpha-2 diretamente.
O único caso de borda que vale a pena tratar é o fato de que nem todos os códigos de duas letras têm emojis de bandeira correspondentes. Códigos como UN e EU, que são reservados na ISO 3166-1 em vez de atribuídos a países, produzem emojis de bandeira que nem todas as plataformas renderizam de forma consistente. A função acima também não faz nenhuma validação: ela converte quaisquer caracteres recebidos, portanto a entrada deve ser conferida com a lista de códigos ISO 3166-1 alfa-2 atribuídos (exatamente duas letras de A a Z) antes da conversão. Restringir a entrada aos códigos atribuídos a países e territórios e testar na plataforma de destino cobre esse caso.
O mapeamento de 150+ países que ainda precisávamos
Gerar emojis de bandeira por um algoritmo baseado em códigos ISO resolve a parte da exibição. Mas também precisávamos armazenar e consultar países de forma estruturada — o widget filtrava países por região, os ordenava por vários critérios e associava cada um a metadados adicionais. Isso exigiu um mapeamento adequado.
O mapeamento que construímos associa códigos ISO 3166-1 alpha-2 a um nome canônico em inglês (para uso interno e registro), um código numérico ISO 3166-1 (para sistemas que exigem identificadores numéricos) e um agrupamento por região. O nome em inglês neste mapeamento nunca é usado como chave de busca — existe apenas para depuração e exibição administrativa em contextos não localizados. Todos os nomes voltados ao usuário são gerados no momento da renderização via NSLocale.current.localizedString(forRegionCode:).
Essa separação é a decisão arquitetônica central. A camada de dados fala em códigos ISO. A camada de apresentação traduz para a localidade atual. Elas nunca se encontram no meio. Uma busca de bandeira, um agrupamento por região, uma ordem de classificação — tudo isso opera em códigos ISO. Apenas o texto final renderizado ao usuário passa pela localização.
Estendendo a correção para outros dados
Uma vez que o bug da bandeira foi corrigido, auditamos o restante da base de código para o mesmo padrão. O mesmo problema apareceu em três outros lugares: um módulo de exibição de moedas que usava nomes de moedas localizados como chaves de cache, um seletor de idiomas que armazenava nomes de idiomas localizados nas preferências do usuário e um módulo de formatação de datas que comparava nomes de meses dependentes de localidade.
A correção em cada caso seguiu o mesmo padrão. Substitua a string dependente de localidade pelo seu equivalente ISO ou IETF estável: códigos de moeda (ISO 4217) em vez de nomes de moedas, tags de idioma BCP 47 em vez de nomes de idiomas localizados, valores de mês numéricos em vez de strings de nome de mês. As strings localizadas passam a ser responsabilidade apenas da camada de apresentação.
Valeu a pena realizar a auditoria de forma sistemática. Cada um desses bugs seria invisível nos testes de localidade inglesa e teria surgido apenas em produção, em dispositivos configurados para outros idiomas. Dado que o aplicativo tinha como alvo um público multilíngue, o impacto teria sido significativo.
O escopo do problema em produção
Uma razão pela qual essa classe de bug persiste é que é invisível em ambientes de desenvolvimento de localidade única. Um desenvolvedor trabalhando na localidade inglesa, construindo um aplicativo para um público internacional, pode escrever e testar o recurso inteiro de exibição de países sem nunca acionar o bug. O bug só se manifesta quando a localidade do dispositivo não corresponde à localidade em que o dicionário ou mapeamento foi originalmente construído — que é precisamente a condição que descreve a maioria dos usuários do público-alvo.
Em um widget especificamente, a lacuna é maior do que em um aplicativo principal porque os widgets são executados em um contexto de extensão restrito. A extensão do widget é executada com a localidade do sistema atual, que pode diferir da localidade que o desenvolvedor testou. A depuração também é menos direta do que no app principal: você executa o esquema da extensão do widget no Xcode (ou usa Debug > Attach to Process), e o código da linha do tempo e da visualização é executado conforme a programação do WidgetKit, em vez de responder às suas próprias interações. Identificar bugs de renderização dependentes de localidade em widgets requer executar a extensão nas localidades relevantes, usar prévias do Xcode com entradas de exemplo da linha do tempo e prestar atenção ao contexto real de renderização da extensão — uma etapa fácil de pular até que um usuário relate o problema em produção.
A resposta prática é tratar toda a lógica de exibição dependente de localidade como uma categoria de risco, não apenas um bug individual para corrigir. A revisão de código deve incluir uma verificação de strings dependentes de localidade usadas como identificadores. A pergunta de revisão é simples: essa string está passando por algum método de localização antes de ser usada como chave ou valor de comparação? Se sim, o design precisa mudar. Códigos ISO, identificadores numéricos e outras referências independentes de localidade devem substituir strings localizadas em cada camada, exceto na exibição final.
Testes em diferentes localidades
A lição mais direta deste bug é que o teste de localidade precisa fazer parte da matriz de teste padrão, não ser algo considerado apenas depois. O iOS torna isso relativamente simples: o simulador suporta a troca de localidade sem mudar o idioma do sistema, e os testes de UI podem ser parametrizados por localidade.
Para exibição de bandeiras e países especificamente, adicionamos testes de snapshot executados com um conjunto fixo de localidades de dispositivo — inglês, alemão, francês, japonês, árabe — e que verificam se o emoji de bandeira para um conjunto fixo de códigos ISO permanece correto em todos eles. Esses testes detectam regressões imediatamente se alguém introduzir uma busca dependente de localidade no caminho de exibição.
O princípio mais amplo é que qualquer recurso que envolva localização, internacionalização ou exibição dependente de região deve ter testes automatizados que exercitem múltiplas localidades. O controle de qualidade manual em uma única localidade não é substituto. A categoria de bugs causados por suposições dependentes de localidade é comum e permanece invisível para testes de localidade única.
Artigos relacionados
- Dynamic Island para apps de temporizador: implementação e lições — outro contexto de extensão iOS onde o comportamento da plataforma difere do que os testes no aplicativo revelam
- Tabelas de fechamento para cálculo de relacionamentos — usando identificadores estáveis (IDs em vez de nomes) para construir lógica robusta de busca e percurso