Sortea2 Blog

  • Zonas horarias y PHP

    Introducción. Métodos sencillos que no funcionan

    Cuando escribimos nuestra página de sorteos consideramos que sería útil que los usuarios pudieran realizar un sorteo a una fecha y hora determinadas. Por si desean darle un poco de emoción o dar claridad a un sorteo. No es lo mismo que tú publiques unos resultados sino que digas «en esta página ofrecerán los resultados». Es una forma de mostrar que no influyes en el resultado.

    Al querer realizar los sorteos a una hora concreta detectamos que era problemática la situación de cara a los usuarios que no usasen la misma zona horaria que nosotros (que es la de España). Una solución sería dar todas las fechas con nuestro horario. Es exigir a los clientes que se adapten a nuestras condiciones. Aunque hay muchos que optan por ella, me parece inadmisible.

    Nuestra primera idea fue la de mostrar un desplegable con zonas horarias, mediante una función sencilla:
    Se muestra una zona horaria e internamente se guarda un valor numérico: el número de horas de diferencia respecto de la hora de Madrid.

    Lisboa(-1)
    Madrid(0)
    Canarias(-1)
    Nueva York(-6)
    México DF (-7)

    Esta aproximación es una de las más frecuentes. Es sencilla y fácil de tratar. Cuando un usuario de México solicita que su sorteo sea a las 13:00 horas, convertimos el horario al de España (sumamos 7 horas) y lo guardamos (20:00 horas). Luego cuando ese usuario decida consultar el sorteo, volvemos a realizar la conversión inversa, restando siete horas.

    Sin embargo la vida no es tan fácil. Quizás el mayor de los problemas es la maligna DST (Daylight Saving Time, Horario de verano en castellano).

    Porque cuando nos acercamos a estos días en que se suma o resta una hora, los resultados pueden ser impredecibles.

    a) Hay países que no cambian nunca de horario (sabia decisión). Entonces respecto a ellos habrá veces que España tenga X horas de diferencia y el resto del año X +1 horas.

    b) Hay países que cambian de horario en fechas diferentes, como sucede con Estados Unidos. No se puede ignorar a Estados Unidos, que tiene más usuarios de castellano que la propia España.
    Estados Unidos es aún más complejo por cuanto las elecciones presidenciales pueden cambiar la fecha en que se realice el cambio de hora. Además, tiene potestad para realizar cambios en el día seleccionado para la modificación horaria, sabemos cuando cambiarán el año que viene, pero dentro de tres años puede cambiar de sistema (ya lo han hecho varias veces). Vamos, que es todo un caso a tratar por separado.

    c) Hay países que incluso cambian la regla. Muchos países decidieron dejar de cambiar de hora hace algún tiempo. De cara al futuro no es problemático pero si queremos hacer una correcta conversión de fechas, su situación debe ser tenida en cuenta.

    d) Hay países que no tienen zonas horarias exactas. Ya sea porque tengan diferencias de cuarto de hora respecto de una zona (como Nepal, +5:45) o Venezuela (-4:30). Además, el caso de Venezuela, en que dicho cambio de zona horaria es muy actual, es problemático para fechas del pasado reciente.

    Por todas estas razones, el método de sumar o restar horas, sencillamente no sirve.


    Zonas horarias y PHP

    Para nuestro caso, al estar desarrollada la página con PHP, tras buscar soluciones posibles por Internet, sin mucho éxito y tratando de hacer alguna cosa bien, optamos por mirar algunas funciones propias de PHP.

    Las funciones propias de PHP para zonas horarias en la versión actual (4.4) son las siguientes:

      # timezone_abbreviations_list
      # timezone_identifiers_list
      # timezone_name_from_abbr
      # timezone_name_get
      # timezone_offset_get
      # timezone_open
      # timezone_transitions_get

    No creemos que sea necesario dar una enumeración de las mismas, máxime cuando se puede consultar la completa documentación online. Lo importante es saber que la forma en que PHP guarda las zonas horarias. Aunque hay posibles conversiones de texto (siempre más fáciles si el texto está en inglés), lo suyo es usar los pares Continente/País que presenta PHP en su lista de zonas horarias soportadas.

    Por ejemplo:

      Europe/Madrid
      America/Mexico_City
      America/Buenos_Aires
      Atlantic/Canary

    La lista de zonas horarias admitidas es extensísima. En muchos casos estas zonas horarias son idénticas, como sucede con Madrid, Berlín o París. De ahí que no tenga mucho sentido mostrar desplegables casi infinitos en sus posibilidades (sólo para la Antártida PHP soporta ¡Ocho zonas horarias!).

    Se realiza una selección de países y ciudades representativas. Lo ideal es mostrar junto a dicha ciudad la zona horaria correspondiente para que así un usuario de un lugar recóndito pueda localizar la zona que mejor se adapte a su ubicación:

      Buenos Aires (GMT -3)

      La Paz (GMT -4)
      Caracas (GMT -4.5)
      Montreal (GMT -5)

    Desplegable zonas horarias
    Desplegable zonas horarias

    Por lo tanto, trabajamos con tres valores:

    a) El código de zona horaria de PHP: America/Buenos_Aires
    b) La descripción para seres humanos de dicha zona: Buenos Aires
    c) Un indicativo para usuarios que no encajen exactamente con dicho país: GMT -3

    Esto ya sugiere que crear una tabla donde se guarde la información de las zonas horarias puede ser de gran ayuda.

    Si te interesa la que usamos en nuestra página, puedes descargártela de aquí, en formato de creación para MySQL (fácilmente adaptable a otras bases de datos).


    Zonas horarias. ¿Qué convertimos?

    Antes de continuar una buena tarea sobre la que meditar es cómo vamos a guardar las fechas y horas en nuestra base de datos. Si nos centramos en la ciudad en que vivimos, nos podemos encontrar con que el servidor que alberga nuestra página esté ubicado en otra zona horaria. Cierto es que a veces podemos manipular esta hora, pero en hostings compartidos, a veces esto no es posible.

    Así, uno puede encontrase con que:

    a) Su zona horaria es de Madrid, España.
    b) Quiere guardar una fecha y hora de Canarias, España.
    c) El servidor está situado en Los Ángeles, Estados Unidos.

    Con lo cual los problemas están casi garantizados.

    Tampoco hay que volverse loco ante las diferencias de fechas. Salvo que tengas una situación tan especial como la nuestra, en que necesitamos programar tareas futuras, quizás puedas sobrevivir sin problemas y sin saber nada de todo esto. Pero el conocimiento siempre es útil.

    En las tablas de datos es común guardar un registro con la fecha y hora de la última modificación. ¿Debemos convertir esa fecha y hora?

    Pues salvo que sean tareas críticas, probablemente no. Las fechas de creación de registro bien se pueden quedar en el horario que tenga el servidor. Sólo aquellas que requieran de ser mostradas en pantalla pueden y deben ser tratadas con mayor consideración. En el caso de sortea2.com, las horas a las que se programan los sorteos. Para ellas no sólo se guardará la fecha y hora, sino también la zona horaria en que se realizaron.

    Por lo tanto, por sentido común, no es muy lógico guardarlas en una horario tan poco estándar como el de Madrid. Hay dos opciones principales:

      Guardar la hora y decir «son las 14:30 con el horario de Buenos Aires».
      Convertirla al estandar GMT (Hora de Greenwich, Inglaterra) e indicar la zona horaria deseada. «son las 16:30, zona horaria preferida Buenos Aires».

    Esta segunda opción nos parece mejor porque permite en un momento dado comparar fechas de inmediato. Del otro modo casi siempre habrá que hacer una conversión antes de poder continuar.

    Como resumen: se guardan las horas en estandar GMT, sólo en los casos en que no sean necesarias manipulaciones posteriores.


    Zonas horarias y PHP. Las transiciones.

    Tras planear el desarrollo de la página, sólo queda un problema: cómo convertir entre dos zonas horarias dadas. No sé cómo lo habrán desarrollado en otros lenguajes de programación, pero en PHP el método es realmente ingenioso y complicado.

    Vaya por delante que en internet hay numerosas clases realizadas por otros que explican cómo realizar estos cambios. Hay incluso clases hechas en PEAR que se supone son totalmente estándar. Tras la nefasta experiencia de usar una de ellas y descubrir que estaba mal (no distinguía los cambios de hora), decidí seguir el camino difícil y propio.

    Siempre es más fácil usar algo ya hecho y que funcione. Y uno puede vivir dignamente sin conocer las transiciones de PHP.

    Pero si quieres continuar, las transiciones son un array de fechas correspondiente a cada una de las zonas horarias. Es un array monstruoso en que se muestran los días del año en que se produce un cambio de hora para dicha zona horaria. Por ejemplo, para la zona horaria Europe/Madrid dice:

    ( [0] => Array ( [ts] => -1661734800 [time] => 1917-05-05T23:00:00+0000 [offset] => 3600 [isdst] => 1 [abbr] => WEST )

    Que traducido al cristiano es algo así como:

    • A la fecha y hora UNIX (segundos desde el 1 de enero de 1970) (correspondiente a GMT, hora estandar de Greenwich) es la de -1661734800).
    • Que se corresponde con la fecha y hora «en inglés» de 1917-05-05 23:00:00+0000.
    • Se produjo un cambio de hora que deja a la hora de Madrid respecto de la de GMT en 3600 segundos (una hora más).
    • Dejando la zona horaria en GMT+1.
    • Quedando en la zona horaria la WEST.

    El listado de transiciones de la zona horaria de Madrid nos avisa de cada cambio de hora que se ha producido en la historia, desde el verano de 1917 hasta el año 2037. Si se necesita tratar fechas anteriores o posteriores, aparte de considerar la opción del suicidio, habría que tirar de enciclopedias y realizar un tedioso trabajo manual.

    Este extensísimo array es la pieza fundamental para realizar conversiones entre zonas horarias en PHP.

    Veamos por ahora cómo obtener el fichero de transiciones para una zona horaria:

    $timezone = «Europe/Madrid»;
    $timezoneinfo = new DateTimeZone($timezone);
    $arraytime = $timezoneinfo->getTransitions();

    En la primera línea definimos la zona horaria a tratar, según las zonas horarias que PHP entiende.

    En la segunda creamos un objeto de tipo zona horaria, relativo a dicha zona inicial.

    En la tercera obtenemos las transiciones: cuándo se produjeron los cambios de hora respecto de GMT para esa zona horaria.

    Es curioso ver el fichero de transiciones. Ahora solemos cambiar de hora los sábados de 2:00 am a 3:00 am, pero en 1917 decidieron hacerlo a las 0:00 horas. Se nota que se trasnochaba menos por aquel entonces. Durante la II Guerra Mundial hubo cambios de hora a las 11:00 de la noche. Luego se volvió a las 12:00 y en los ochenta se pasó a hacerlo a la 1:00 am.

    Como podemos ver, y evitemos dispersarnos, los cambios de hora son una verdadera pesadilla en lo que al horario de verano se refiere.

    Con el array de transiciones ya es más o menos fácil. Si tenemos una fecha en formato GMT, junto con una zona horaria deseada y deseamos convertirla a la hora propiamente dicha, basta con:

    Tomamos la hora en GMT y recorremos el array de transiciones, hasta que encontramos una hora superior a la nuestra.
    Por ejemplo, si tenemos las 20:00 del 15 de Enero de 2012, y el horario deseado es el de Madrid, vemos que el último cambio de horas se produce el 30 de octubre del 2011. Y que entonces España tendrá una hora más que la hora GMT. Esto sucederá así hasta el 25 de Marzo del 2012, por lo que efectivamente, si queremos convertir esa hora en GMT a hora de Madrid, debemos sumarle una hora (3600 segundos) más.

    [110] => Array ( [ts] => 1319936400 [time] => 2011-10-30T01:00:00+0000 [offset] => 3600 [isdst] => [abbr] => CET )
    [111] => Array ( [ts] => 1332637200 [time] => 2012-03-25T01:00:00+0000 [offset] => 7200 [isdst] => 1 [abbr] => CEST )

    Este proceso es mucho más delicado si la hora está en el mismo borde del cambio de hora. Por ejemplo, si queremos convertir las 01:00 horas del 25 de marzo de 2012, de horario GMT al horario de Madrid.

    Para afinar completamente la regla es la siguiente:

    • Partimos de una hora.
    • Recorremos las transiciones hasta pasarnos de dicha hora.
    • Miramos el registro anterior. Obtenemos la diferencia de segundos.
    • Se la sumamos a la hora que teníamos. Si nos hemos pasado de la siguiente transición, tomamos esa siguiente, sino (lo habitual) nos quedamos con la que teníamos.

    La verdad es que esto se entiende mejor con el código que escrito con palabras:

    $timezoneinfo = new DateTimeZone($timezone);
    $arraytime = $timezoneinfo->getTransitions();

    $i = 1;

    foreach ($arraytime as $transicion)
    {
    $i ++;
    if ($transicion[ts] + $arraytime[$i][offset] >= $gmttime)
    {
    $indice = $i –1;
    break;
    }
    }
    $newtime = $gmttime + $arraytime[$indice][offset];

    Nos recorremos el array de transiciones. Cuando la transición + las horas de diferencia sean mayores que nuestra fecha de partida, quiere decir que hemos llegado y es el momento de tomar ese valor, sumarlo a la fecha que teníamos y voilá, ya hemos convertido la fecha.

    Zonas horarias y PHP. Funciones de conversión.

    Básicamente ya hemos expuesto todo el proceso. Lo único necesario son dos funciones: una que parta de una zona horaria y una hora expresada en dicha zona horaria y la transforme en GMT y otra que partiendo de una hora en GMT y una zona horaria, realice la transformación inversa.

    Pueden servir las dos siguientes funciones:

    function of_gmttolocaltime ($gmttime, $timezone)
    {
    $timezoneinfo = new DateTimeZone($timezone);
    $arraytime = $timezoneinfo->getTransitions();

    $i = 1;

    foreach ($arraytime as $transicion)
    {
    $i ++;
    if ($transicion[ts] + $arraytime[$i][offset] >= $gmttime)
    {
    $indice = $i –1;
    break;
    }
    }
    $newtime = $gmttime + $arraytime[$indice][offset];
    return $newtime;
    }

    /**
    * Entrada. Una hora en formato UNIX de una zona horaria, y el nombre de la zona horaria.
    * Salida. La hora convertida a GMT.
    */

    function of_localtimetogmt ($localtime, $timezone)
    {
    $timezoneinfo = new DateTimeZone($timezone);
    $arraytime = $timezoneinfo->getTransitions();

    $i = –1;

    foreach ($arraytime as $transicion)
    {
    $i ++;
    if ($transicion[ts] – $arraytime[$i][offset]>= $localtime)
    {
    $indice = $i –1;
    break;
    }
    }
    $newtime = $localtime – $arraytime[$indice][offset];
    return $newtime;
    }

    Las horas siempre se expresan en horario UNIX. ¡Bastante quebradero de cabeza dan las zonas horarias para sufrir aún más con los formatos locales de fecha y hora!

    Creo que ha quedado todo bien expresado pero si tienes alguna duda o aclaración déjala en los comentarios. Las correcciones también son bienvenidas.

    Actualización. Tras dos años usando este sistema hemos visto bastantes claroscuros que exponemos en esta otra entrada: problemas con las zonas horarias y PHP.

  • Plataforma multi-idioma en PHP

    Plataforma multi-idioma

    Os voy a hablar de cómo hemos implementado en www.sortea2.com la plataforma multi-idioma, es un sistema bien simple, pero totalmente funcional.

    Lo primero que debemos tener en cuenta es las cosas que necesitamos:

    1. Detectar el idioma del usuario para mostrarle el idioma que mejor le convenga.
    2. Tener los textos escritos en todos los idiomas que necesites utilizar.
    3. Darle la opción al usuario de que pueda cambiar de idioma en el momento en que quiera.


    Detectar idioma del usuario

    Para detectar el idioma del usuario, en www.sortea2.com decidimos detectar el idioma del navegador, porque era una de las maneras más simples y útiles. Para ello, con utilizar simplemente la siguiente variable de servidor ya podemos obtener el idioma del navegador.

    $idioma = $_SERVER[‘HTTP_ACCEPT_LANGUAGE’];

    El problema es que habitualmente, en casi todos los navegadores, lo que se nos devuelve por esa variable de servidor es el idioma y su derivación, algo del tipo: «es-es», a nosotros lo que nos interesa son únicamente las dos primeras letras, que son las que nos dicen el idioma propiamente dicho.

    Para ello, podemos realizar un simple parseo de las dos primeras letras, tal y como se muestra a continuación:

    $idioma = substr($idioma, 0, 2);

    Bien, ahora tenemos en la variable $idioma una cadena que será algo así como «es» o «en». Ya hemos obtenido el idioma del navegador.


    Tener los textos escritos en todos los idiomas que se necesiten

    Hay varias opciones para tener los textos escritos en algún lugar para poder acceder a ellos dependiendo de qué idioma se necesite. Se podría hacer mediante base de datos o como nosotros optamos: con archivos de constantes de php. Este método es más eficiente que el de las conexiones a base de datos, porque optimiza los recursos del servidor y porque los archivos de constantes se guardan el la memoria del servidor y no hace falta estar llamandolos continuamente.

    Hace falta tener tantos archivos de constantes como idiomas se desee poder utilizar. Estos archivos tienen que tener esta estructura:

    Archivo es.php

    <?php

    define (SORTEOS_Y_APUESTAS_ENTRE_COLEGAS, «Sortea2 sorteos y apuestas entre colegas»);

    define (VER_SORTEOS_PUBLICOS, «ver sorteos públicos»);

    define (AVANZADOS, «sorteos avanzados»);

    ?>

    Archivo en.php

    <?php

    define (SORTEOS_Y_APUESTAS_ENTRE_COLEGAS, «Sortea2: Raffles and betting with colleagues»);

    define (VER_SORTEOS_PUBLICOS, «public raffles»);

    define (AVANZADOS, «advanced raffles»);

    ?>

    Hacer que se abra un archivo de idioma u otro dependiendo del navegador

    Esta es la parte más simple. Lo único que hay que hacer es que si $idioma es «es», que se abra el archivo de idioma es.php, y si es «en», que se abra en.php

    En nuestro caso, como nuestra página solo está disponible en español y en inglés, lo que hemos hecho es que si el idioma del navegador no es español, entonces que se abra el archivo de idiomas inglés. En caso de tener la necesidad de dividir los idiomas en varios archivos, porque se necesitan para lugares independientes, o por lo que sea, se puede pasar como parámetro opcional la variable $string, que se puede utilizar para usar archivos de idioma del tipo «es-sorteos.php» o «en-sorteos.php», si la variable está vacía, entonces se abrirá el archivo «es.php», lógicamente.

    if ($idioma == «es»)

    {

    require_once(PHPPATH.«/langs/es».$string.«.php»);

    }

    else

    {

    require_once(PHPPATH.«/langs/en».$string.«.php»);

    }

    Todo se debería tener en una función en un archivo que se suela llamar en todas lás páginas de su web. Por ejemplo, una función que se llamase of_idioma() que devolviese la variable $idioma, por si se tiene que utilizar en algún momento, en alguna página de su web.

    Método de utilizarlo

    Una vez llamada la función of_idioma(), podemos llamar a los textos refiriéndose a ellos por medio del nombre de la constante. O osea, para escribir «Sortea2 – sorteos y apuestas entre colegas», en vez de escribir la frase directamente, hay que escribir la constante, que dependiendo del archivo de idioma que se haya abierto, tendrá un significado u otro.

    of_idioma();

    echo «Sortea2 – sorteos y apuestas entre colegas»;//No funcionaría

    echo SORTEOS_Y_APUESTAS_ENTRE_COLEGAS; //Sí funcionaría


    Resumen

    Así, lo que hay que tener en cuenta es lo siguiente:

    a) El idioma en principio lo capturamos del navegador.

    b) En función del idioma, incluimos un fichero de constantes u otro. Si la página es muy extensa, podemos usar distintos ficheros de constantes y siempre haremos una de esas distinciones: si el idioma es español, carga las constantes en español, si es inglés, las constantes en inglés.

    c) Para escribir los textos, usamos en lugar de literales los valores de las constantes. Como regla nemotécnica es bueno que los nombres de las constantes sean parecidos a los textos que representan.


    Mejorando la experiencia del usuario. Cookie de idioma

    Puede y suele ocurrir que el idioma del navegador no sea el que prefiera el usuario. Para ello en sortea2 incluimos al pie de todas las páginas un link para cambiar de idioma. Si la página está en español, el link dice «Read this web in English». Si está en inglés «Ver esta página en español».

    Cuando un usuario realiza la selección de idioma, clickeando en dicho link, lo que hacemos es crear una cookie. Mediante dicha cookie que se guarda en el disco del ordenador del usuario que visita la página, almacenamos la preferencia de selección de idioma.

    Por ello, en lugar de usar siempre el idioma del navegador hacemos lo siguiente:

    a) Si el usuario tiene la cookie guardada, ese idioma prevalece.

    b) De lo contrario, usamos el idioma del navegador.

    Guardar la cookie es algo relativamente sencillo y de lo que pueden encontrarse numerosos ejemplos en la web. Como nosotros hemos empleado Jquery y el plugin de jquery.cookie, el código es peculiar aunque también ilustrativo:

    $(document).ready(function(){

    $(«a[name=’cooken’],a[name=’cookes’],a[name=’cookde’]»).
    click(function(event)

    {

    var cookiename = ‘lan’;

    var lang = $(this).attr(«name»);

    language = lang.substr(lang.length -2, 2);

    event.preventDefault();

    cookieval = $.cookie(cookiename,language,{ expires: 7 , path: ‘/’});

    window.location.href=window.location.href;

    });

    });

    Con este código, cuando se pulsa uno de los links de idioma, primero impedimos que se desplace a la ruta que indica ese link (preventdefault).

    Luego seleccionamos las últimas letras del nombre de dicho link, nos servirán para determinar el idioma a guardar. Observar que el ejemplo también está preparado para el idioma «de», por si queremos traducirla al alemán.

    Con esas dos letras, generamos una cookie, que se llama «lan» y durará 7 días.

    Y tras realizar todo esto, recargamos la ventana. Esta vez tendremos la cookie ya guardada y al recargarse lo reconoceremos y lo haremos en el idioma del usuario. Y como la cookie permanece en el sistema, su selección de idioma permanecerá.


    Determinando el valor de la cookie

    Ya hemos recorrido todo el camino. Ahora basta con juntar las piezas. El proceso es muy sencillo.

    1. Cargamos la página.

    2. ¿Tenemos cookie? Si es así, tomamos el idioma.

    3. Si no hay cookie, tomamos el idioma del navegador.

    4. Con ese valor de idioma cargamos un fichero de constantes u otro.

    5. Nos referimos siempre a los literales con valores de constantes.

    El código final es realmente simple:

    $idioma = isset($_COOKIE[‘lan’]) ? $_COOKIE[‘lan’] : «»;

    if ($idioma == «»)

    {

    $idioma = $_SERVER[‘HTTP_ACCEPT_LANGUAGE’];

    $idioma = substr($idioma, 0, 2);
    }

    if ($idioma == «es»)

    {

    require_once(PHPPATH.«/langs/es».$string.«.php»);

    }

    elseif ($idioma == «en»)

    {

    require_once(PHPPATH.«/langs/en».$string.«.php»);

    }

    ¿Algo no te ha quedado claro? Pregúntanoslo en los comentarios y te trataremos de ayudar.

  • Sistema de búsqueda

    Hemos implantado un sistema de búsqueda de sorteos. No es ni mucho menos perfecto, pero es totalmente funcional.

    También hemos deshabilitado el link a la página actual en el menu horizontal. Además de numerosos cambios internos.

  • Página perfectamente visible en Internet Explorer

    A diferencia a como estaba todo anteriormente, ahora se puede visualizar la pagina en Internet Explorer sin ningún problema con la hoja de estilos. Algunos elementos se ven diferente, como las tabs, que no cambian de color al ser pulsadas, o cambios de margin o padding que salían desajustados.

  • Sistema de envio de errores y votos en AJAX

    Hemos creado un formulario para que nuestros usuarios nos avisen de posibles erores que se nos hayan podido pasar, ha habido que ponerle un recaptcha para evitar el spam.

    Luego, también hemos hecho que los votos se añadan directamente, sin cambiar de página ni nada, esa parte está practicamente cerrada, solo queda modificar el color del nº de votos dependiendo de si son positivos o negativos.