Trabalhando de acordo com a Localidade em PHP

Artigo que explica como realizar operações em PHP que variam de acordo com a localidade.

Introdução

Muitas aplicações web possuem uma área de abrangência bastante restrita, normalmente aos usuários de uma entidade ou pessoas de um país. Porém, também existem aplicações mais globais, usadas por pessoas de diferentes países e que possuem notações próprias para trabalhar com palavras e números.

A questão da internacionalização de uma aplicação envolve várias tarefas, como a tradução dos termos utilizados, possíveis reformulações de layout, e a aplicação de regras relacionadas à localidade. Neste artigo vamos tratar especificamente desta última, que são as regras relacionadas à localidade.

A função setlocale

A função setlocale é responsável por especificar o tipo de localidade desejada para realizar operações de uma categoria. O primeiro parâmetro representa a categoria de operações, e deve valer uma das constantes abaixo:

  • LC_COLLATE - para especificar as regras da localidade para comparação de textos.
  • LC_CTYPE - para especificar as regras da localidade para classificação/conversão de caracteres.
  • LC_MONETARY - para especificar a notação monetária de uma localidade.
  • LC_NUMERIC - para especificar a notação numérica de uma localidade.
  • LC_TIME - para especificar a notação de data/tempo de uma localidade.
  • LC_MESSAGES - para especificar o idioma das mensagens de log.
  • LC_ALL - para especificar a localidade para todas as categorias.

O segundo parâmetro deve ser uma string, com o nome da localidade de acordo com o sistema operacional. Isso porque a função setlocale utiliza as definições de localidade definidas no servidor, inclusive seu nome.

Por exemplo, no Linux, podemos instalar as definições de localidade para português do Brasil e modificar as regras de localidade com a string "pt_BR", enquanto, no Windows, a string pode ser "Portuguese_Brazil". O charset também é importante, já que algumas operações dependem dele.

Podemos passar um terceiro, quarto, quinto parâmetro, etc. para a função setlocale, especificando várias localidades possíveis. A primeira a ser encontrada pelo servidor é utilizada. Veja o exemplo:

setlocale(LC_ALL, 'pt_BR.UTF-8', 'Portuguese_Brazil.1252');

Para sabermos qual a localidade que está sendo usada no momento, basta passarmos como segundo parâmetro o valor "0", na forma de string, conforme o exemplo:

$localidade_atual_numeric = setlocale(LC_NUMERIC, '0');

E, finalmente, podemos passar o valor "C" como segundo parâmetro para especificar a localidade default do PHP, que usa a notação americana para várias coisas, mas, mesmo assim, possui diferenças em relação à localidade "en_US".

Para obter as características de uma localidade, basta definirmos a localidade desejada e chamar a função localeconv, que retorna array associativo com os símbolos e algumas regras usadas pela localidade.


Comparando textos

Para compararmos textos de acordo com a localidade, utilizamos a categoria LC_COLLATE. Ao definirmos uma localidade nesta categoria, podemos usar a função strcoll para comparar duas strings, para saber qual deve vir antes que a outra quando forem ordenadas, por exemplo. O funcionamento de strcoll é parecido com strcmp, porém, ela leva em consideração a localidade para determinar o resultado (comparando letra a letra), enquanto strcmp compara o valor binário de cada byte da string, da esquerda para direita.

Para entender exatamente o que ocorre, vejamos o teste a seguir (considerando que estamos usando a codificação UTF-8):

// Comparação binária
$comparacao1 = strcmp('índio', 'laranja');

// Comparação de acordo com a localidade
setlocale(LC_COLLATE, 'pt_BR.UTF-8');
$comparacao2 = strcoll('índio', 'laranja');

A variável $comparacao1 obteve o valor "87", indicando que "índio" deve vir após "laranja", se colocados em ordem. Já a variável $comparacao2 obteve o valor "-3", indicando que "índio" deve vir antes de "laranja", se colocados em ordem. O valor de $comparacao2 é mais adequado para o português, já que a letra "i" vem antes da letra "l", mesmo que ela tenha acento.

Para entender como é feita a comparação byte a byte, precisamos quebrar as strings "índio" e "laranja" em bytes, segundo a codificação UTF-8:

Bytes da palavra "índio" em UTF-8
Letra í n d i o
Bytes 11000011 10101101 01101110 01100100 01101001 01101111
Bytes Decimal 195 173 110 100 105 111
Bytes da palavra "laranja" em UTF-8
Letra l a r a n j a
Bytes 01101100 01100001 01110010 01100001 01101110 01101010 01100001
Bytes Decimal 108 97 114 97 110 106 97

Observando as tabelas, basta comparar o primeiro byte de uma string com o primeiro byte da outra string, ou seja, 195 e 108. Como o 195 é maior que 108, então a função strcmp retornou um valor maior que zero, ou seja, a primeira string é posterior à primeira string. Ou seja, a ordem fica condicionada ao algorítimo de codificação dos caracteres que, no caso, foi o UTF-8

Já a função strcoll considera a letra "í" com acento e a letra "i" sem acento como sendo uma letra da mesma "família". Embora se compararmos estas duas letras, a "í" com acento virá após a "i" sem acento, porém, a "í" com acento virá antes da letra "l", que está mais a frente no alfabeto.

Agora que entendemos bem a diferença entre as duas funções, podemos utilizá-la num caso mais prático, como a ordenação de um array com strings codificadas em UTF-8, levando em consideração as regras de comparação de uma localidade:

<?php

$array = array(
    'índio',
    'aluguel',
    'laranja',
    'carro'
);

setlocale(LC_COLLATE, 'pt_BR.UTF-8');
usort($array, 'strcoll');

Ao chamarmos a função usort, podemos passar um callback informando a função que usaremos para comparar dois elementos do array durante a ordenação. Como definimos uma localidade e depois chamamos a função usort passando o callback "strcoll", então o array será ordenado de acordo com a localidade, resultando em: aluguel, carro, índio, laranja.


Classificação/conversão de Caracteres

Cada localidade possui a classificação de seus símbolos e algumas correlações entre eles. Por exemplo, a letra "a" minúscula está na categoria de "letra" e está relacionada à letra "A" maiúscula. Em português, usamos acentuação em algumas letras e esta correlação também se aplica. Por exemplo, a letra "é" minúscula e acentuada está relacionada à letra "É" maiúscula e acentuada. Em outras localidades, este correlação pode variar.

Neste caso, a categoria LC_CTYPE afeta a classificação de caracteres e funções que dependem da tabela de correlação entre elas. Ou seja, as funções da extensão ctype, e algumas funções como strtoupper, strtolower, ucfirst, ucwords, escapeshellarg, etc. Veja o exemplo:

setlocale(LC_CTYPE, 'pt_BR.ISO-8859-1');
$alpha = ctype_alpha('ã');           // Recebe valor "true"
$maiuscula = strtoupper('avião');    // Recebe valor "AVIÃO"

Infelizmente, a linguagem PHP ainda não trabalha nativamente com unicode para algumas funções. Existe a esperança de que o PHP 6 isso se resolva, mas, enquanto isso, é necessário usar recursos específicos para algumas operações. Por exemplo, para converter uma string UTF-8 para maiúscula, pode-se usar a extensão Multibyte, conforme exemplo:.

$maiuscula = mb_strtoupper('avião', 'UTF-8');

Formatação de números

Para formatação de números de acordo com a localidade, especificamos a localidade para a categoria LC_NUMERIC. Com isso, serão usados os símbolos adequados para separador de casas decimais e separador de milhar. Em português do Brasil, o separador de casas decimais é a vírgula, enquanto o separador de milhar é o ponto final. Veja o exemplo:

$float = 1500.87;

setlocale(LC_NUMERIC, 'pt_BR.UTF-8');
echo $float;     // Imprime 1.500,87

Observação: em alguns servidores, a definição de localidade pode não usar o ponto final como separador de milhar.

Cuidado: note que, ao definir a localidade, você precisa tomar cuidado ao imprimir números. Lembre-se que eles serão impressos na notação da localidade. Para garantir que um número seja impresso na notação do PHP, você pode voltar a localidade para "C", temporariamente:

$float = 1500.87;

$locale_antigo = setlocale(LC_NUMERIC, '0');
setlocale(LC_NUMERIC, 'C');

echo $float;     // Imprime 1500.87

setlocale(LC_NUMERIC, $locale_antigo);

Formatação Monetária

De forma similar à definição da notação numérica com LC_NUMERIC, existe a categoria LC_MONETARY para definir a notação monetária de alguma localidade. Por exemplo, no Brasil, utilizamos a notação "R$ x.xxxx,xx". Definindo a localidade nesta categoria, modificamos o comportamento da função money_format:

$float = 1500.87;

setlocale(LC_MONETARY, 'pt_BR.UTF-8');

echo money_format('%n', $float);   // Imprime: R$ 1.500,87
echo money_format('%i', $float);   // Imprime: BRL 1.500,87

Observação: a função money_format não está definida em todos os sistemas. Veja uma solução alternativa para money_format.


Formatação de Data

Algumas flags usadas pela função strftime dependem da localidade. Por exemplo, a flat "%A" representa o nome do dia da semana. Se definirmos a localidade para a categoria LC_TIME, obtemos o nome correto para as datas, conforme o exemplo:

$time = mktime(0, 0, 0, 9, 26, 2012);

setlocale(LC_TIME, 'pt_BR.UTF-8');
echo strftime('%A, %d de %B de %Y', $time); // Imprime: quarta, 26 de setembro de 2012

Considerações Finais

Tornar a aplicação adequada à localidade a aproxima da realidade do usuário, sendo mais amigável e usual. Porém, alguns cuidados devem ser tomados, como visto no artigo Transformação de Dados.

22 comentários

Diego Figueiró Dias disse...

Olá Rubens,

Tentei fazer o teste com a função ctype_alnum e ctype_alpha, mas os caracteres acentuados acabam retornando false, mesmo configurando o setlocale como está no artigo.

Estou testando no Windows utilizando IIS 7.5, não sei se tem algo a ver, já que não cheguei a testar em ambiente Linux.

Valeu e parabéns pelo artigo.

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Diego.

Você está utilizando qual codificação? Pois, como falado no artigo, as funções ctype do PHP ainda não trabalham com codificações Unicode, como UTF-8.

Diego Figueiró Dias disse...

Olá Rubens,

Primeiro testei conforme o exemplo, que acredito que não tenha dado certo por ser Windows (locale pt_BR.ISO-8859-1).

Depois verifiquei que o locale padrão aqui é o Portuguese_Brazilian.1252, como mencionou logo no início do artigo que seria para sistemas Windows e que também não está funcionando aqui com ctype.

Será que tenho que passar um parâmetro diferente desse para setlocale no Windows para que funcione?

Valeu.

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Diego.

Se não me engano, a codificação 1252 no Windows é correspondente à UTF-8, por isso as funções ctype não vão funcionar. Não me lembro o código para ISO-8859-1.

Diego Figueiró Dias disse...

Oi Rubens,

Acredito que não tenha jeito mesmo. O código equivalente para o ISO-8859-1 para Windows é 28591, mas testei no Windows 7 e no Windows Server 2008 e não funcionou. O negócio é buscar sempre utilizar o PHP no Linux mesmo.

Aqui tem uma listagem das codificações para Windows, caso alguém precise: http://msdn.microsoft.com/en-us/goglobal/bb964653.

Valeu.

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Diego.

Pode ser que seja uma limitação do sistema mesmo. Faz tempo que não uso mais Windows, então realmente não tenho como ajudar.

Em todo caso, obrigado pela contribuição.

Italo Veloso disse...

Olá, Rubens.

Estou tetando fazer operações com casa decimais, mas sem sucesso nos resultados. Quando tento fazer um soma entre 1.000,00 + 0,12 o resulta é 1. Testei com bcadd, mas também o resultado não foi satisfatório. Tem alguma dica para esse caso?

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Italo.

Para realizar operações no PHP, você precisa usar a notação dele (usando ponto como separador de casas decimais). Depois, para mostrar o resultado, pode usar os recursos mostrados neste post para formatar de acordo com a localidade.

Veja este outro post que deve ajudar:
http://rubsphp.blogspot.com.br/2009/12/tipo-float.html

Michael Felix Dias disse...

Gosto muito dos seus artigos pois além de ser muito didático os assuntos são bastante curiosos/iteressantes, algo difícil de encontrar em blogs brasileiros (até onde eu sei). Gostaria que você passasse algumas referências suas de leitura, pode ser?

Rubens Takiguti Ribeiro (autor do blog) disse...

Olá, Michael

Obrigado pelo elogio. O que acontece é que muitos blogs de PHP dão ênfase nos assuntos mais populares, provavelmente para ganhar mais visitas. Eu procuro equilibrar assuntos básicos e incomuns, mas tentando sempre aprofundar nos conceitos.

Sobre referências de leitura, nada melhor que o próprio manual do PHP (php.net) e ficar ligado na lista de mudanças a cada versão lançada. Além disso, tentar implementar coisas variadas (mesmo coisas que já existem), pode ser uma boa fonte de demanda por pesquisas e acaba te forçando a aprender coisas novas.

Michael Felix Dias disse...

Olá Rubens, você tem toda razão. E quanto ao fato de acompanhar o manual do PHP é um hábito que eu tenho que começar o quanto antes.
Muitíssimo obrigado pela dica. Continuarei acompanhando todos seus artigos e, adoraria conhecê-lo pessoalmente no PHP Conference 2012. Você estará lá, certo? Abraço.

Rafael Gonçalves disse...

ao utilizar a função

strftime('%a, %x às %X', '2009-07-25 13:09');

obtenho um erro na exibição do dia da semana, que aparece com um ponto de interrogação onde deveria ter um acento, no caso "sáb" aparece como "s?b". sabe dizer por quê?

obrigado

Rubens Takiguti Ribeiro (autor do blog) disse...

Oi, Rafael
Esse é provavelmente um problema de diferença de codificação. Você especificou uma (passada junto com a localidade no setlocale) e deve estar exibindo em um local com outra. Sugiro a leitura deste artigo:
http://rubsphp.blogspot.com.br/2011/07/problemas-com-charset-nunca-mais.html

E se quiser se tornar um expert em codificações, especialmente UTF-8, leia esse:
http://rubsphp.blogspot.com.br/2010/10/entendendo-o-unicode-utf8.html

Rafael Gonçalves disse...

Rubens, obrigado pelo retorno.

já verifiquei os detalhes que você citou, e as localidades estão, no meu entendimento configuradas corretamente.

Solicito, se possível, que dê uma olhada no código que postei no pastebin, que contem o exato arquivo que estou executando e que acontece o erro.

http://pastebin.com/S0hp3qzi

não há inserções, ou qualquer tipo de cabeçalho sendo enviado antes, pois, como eu disse, executo ele diretamente.

também já verifiquei o formato em que o arquivo foi salvo, e está em utf-8 sem BOM, porém já tentei com o BOM também e não deu certo.

obrigado

Rubens Takiguti Ribeiro (autor do blog) disse...

Oi, Rafael
Copiei e colei seu código em um arquivo, executei no terminal e apareceu tudo certinho pra mim.
Eu uso Linux e no terminal, também é necessário especificar a codificação.
No meu caso, em "Terminal" > "Definir codificação de caractere" > "Unicode (UTF-8)".

Também executei como uma página web e funcionou também. Aparece "Sáb" com acento normalmente.

Rafael Gonçalves disse...

cara, mas que coisa... será q é por que a máquina de testes é um servidor windows? só pode né... vou subir para um servidor linux amanha e ver o que acontece

segue uma imagem do ocorrido...

http://www.casimages.com.br/i/150220121404992414.png.html

obrigado pela atenção

Rubens Takiguti Ribeiro (autor do blog) disse...

Oi, Rafael
No Windows precisa definir "Portuguese_Brazil.1252", como no exemplo do artigo:

setlocale(LC_ALL, 'pt_BR.UTF-8', 'Portuguese_Brazil.1252');

Troque a linha e veja se resolve.

Para checar se foi definido corretamente, experimente exibir o que foi definido:
var_dump(setlocale(LC_TIME, '0'));

Rafael Gonçalves disse...

Rubens, já havia feito essa alteração, e fiz novamente agora, e não obtive sucesso.

var_dump(setlocale(LC_TIME, '0')); // retorna Portuguese_Brazil.1252

Rafael Gonçalves disse...

Rubens, subi o arquivo hoje para um servidor linux e contatei o que vc disse. O comportamento realmente é diferente do que em um servidor windows. Agora, o porquê de não funcionar corretamente em um servidor windows eu não sei. Obrigado pela ajuda.