jueves, 13 de diciembre de 2012

Caso real de internacionalización de una aplicación ASP.NET

Recientemente he tenido que desarrollar un módulo que trabaja con números de semana. El obtener el número de semana que corresponde a una fecha no es difícil. Sin embargo, obtener el primer y último día de un número de semana es algo más complicado. Además y a nivel de base de datos, trabajo con Sql Server y tengo la necesidad de calcular también números de semana desde T-SQL.

Para obtener el número de semana que corresponde a una fecha, podemos utilizar un método como el siguiente:

static DayOfWeek GetFirstDayOfWeek()

{

    return CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;

}

static int GetWeekOfYear(DateTime aDate)

{

    var firstDayOfWeek = GetFirstDayOfWeek();

    var weekOfYear = CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(

        aDate, CalendarWeekRule.FirstDay, firstDayOfWeek);

    return weekOfYear;

}

Lo más relevante de este código es:

  • Con GetFirstDayOfWeek no se asume que el lunes será el día de comienzo de una semana, sino que se consulta el valor establecido por la cultura actual del subproceso. La cultura actual podría o no coincidir con la configuración regional del equipo, imagina una aplicación web donde según una selección del idioma se establece la cultura del subproceso.
  • Con el valor CalendarWeekRule.FirstDay se establece como se quiere calcular las semanas.

Realmente el enumerado CalendarWeekRule tiene 3 posibles valores, pero el más intuitivo y sencillo de comprender es FirstDay (aunque quizás y según los requerimientos de cada negocio podría no ser el más adecuado). Para ver el detalle de los 3 tipos de cálculo disponibles http://msdn.microsoft.com/en-us/library/system.globalization.calendarweekrule(v=vs.95).aspx. Si quieres cambiar el valor predeterminado de la propiedad CalendarWeekRule del objeto CultureInfo (no confundir con el parámetro del método GetWeekOfYear), tendrás que crear una nueva cultura a partir de la actual, especificar como se calcularán las semanas y después asignársela al subproceso actual (aunque insisto esto no es necesario pero si trabajas por ejemplo con Time Period Library for NET podría ser aconsejable http://www.codeproject.com/Articles/168662/Time-Period-Library-for-NET)

static void CreateCultureInfo(CalendarWeekRule calendarWeekRule, DayOfWeek firstDayOfWeek)

{

    var newCultureInfo = new CultureInfo(CultureInfo.CurrentCulture.Name);

    newCultureInfo.DateTimeFormat.CalendarWeekRule = calendarWeekRule;

    newCultureInfo.DateTimeFormat.FirstDayOfWeek = firstDayOfWeek;

    Thread.CurrentThread.CurrentCulture = newCultureInfo;

}

Además hay otro motivo importante por el que se ha escogido el valor FirstDay y es que la función DATEPART de T-SQL parece que funciona con este valor de forma predeterminada. Lo cierto es que aunque no haya podido confirmarlo oficialmente de esto, los ejemplos me dicen que es así porque los números de semana T-SQL coinciden con los de .NET (la verdad es que no se por que valor del enumerado anterior se rige el cálculo de SQL ni si tiene que ver algo el idioma de la instalación o la configuración regional del equipo…). Sinceramente, espero no tener que utilizar DATEPART con wk en ningún script de T-SQL.

SELECT DATEPART(wk,'20121231')

SELECT DATEPART(wk,'20130101')

SELECT DATEPART(wk,'20130107')

 

 

.NET

T-SQL

31/12/2012

54

54

01/01/2013

1

1

07/01/2013

2

2

Algo a tener muy en cuenta cuando se trabaja en T-SQL y con la función DATEPART es el valor del día marcado como inicio de la semana y que además viene determinado por el valor del idioma de la conexión (fíjate que nada tiene que ver con la configuración regional del equipo ni la cultura del subproceso).

Con DBCC USEROPTIONS se puede consultar estos valores y ver como en us_english el primer día de la semana es el domingo.

language

datefirst

Español

1

us_english

7

Para establecer el día de inicio de una semana tenemos varias posibilidades:

Una vez ya disponemos de cierta sincronización entre el cálculo que realiza .NET y el cálculo que realiza T-SQL, nuestro siguiente método nos ayudará a calcular el día de inicio y el día de fin de un número de semana (el código está prestado pero “entendido” y levemente “modificado” desde http://stackoverflow.com/questions/11309715/get-the-dates-for-getweekofyear-in-c). Puedo asegurar que funciona con CalendarWeekRule.FirstDay pero con el resto de valores del enumerado no lo he probado. También lo he probado con FirstFourDayWeek y funciona igualmente.

static DateTime GetFirstDateOfWeek(DateTime aDate)

{

    var firstDateOfWeek =

        aDate.AddDays(-((aDate.DayOfWeek - GetFirstDayOfWeek() + 7) % 7));

    if (firstDateOfWeek.Year != aDate.Year)

        firstDateOfWeek = new DateTime(aDate.Year, 1, 1);

    return firstDateOfWeek;

}

static DateTime GetLastDateOfWeek(DateTime aDate)

{

    var lastDateOfWeek =

        aDate.AddDays(((GetFirstDayOfWeek() - 1) - aDate.DayOfWeek + 7) % 7);

    if (lastDateOfWeek.Year != aDate.Year)

        lastDateOfWeek = new DateTime(aDate.Year, 12, 31);

    return lastDateOfWeek;

}

Hasta aquí llegó la teoría del cálculo de números de semana en ASP.NET y T-SQL, pero ahora es necesario hacer un ejercicio de reflexión para sopesar los pros y contras de esta solución en una aplicación ASP.NET con los siguientes requerimientos:

  • Una sola instancia de aplicación (modelo Saas).
  • Una base de datos por cliente.
  • Cada cliente tiene una Culture fija que no puede cambiar y que es la nativa al país de origen del cliente.
  • La aplicación tiene que soportar la selección por parte del usuario de las UICulture español e inglés, con independencia de la Culture establecida para el cliente.

Imaginemos que tenemos los siguientes clientes en la aplicación:

 

UICulture

Culture

USA

en o es

en-US

España

en o es

es-ES

Portugal

en o es

pt-PT

La UICulture es seleccionable por el usuario porque es indiferente para la aplicación en que idioma quiere visualizar la interfaz. A grandes rasgos, la elección de una UICulture u otra se traduce casi exclusivamente en la carga de determinados ficheros de recursos .resx.

La Culture tiene que ir fija por cada cliente porque determina el formato de fechas, números… y también como se comportan los cálculos con fechas y semanas. Aquí está claro que el usuario no puede cambiar la Culture porque, por ejemplo, no puedes pasar de ver € a ver $ sin ningún tipo de conversión. Tampoco puedes pasar de ver día/mes/año a mes/día/año sin que el usuario se lleve las manos a la cabeza. En definitiva, si vives en USA siempre trabajarás con $ y tus fechas siempre serán mes/día/año… otra cosa distinta es que quieras practicar el español y puedes cambiar la UICulture cuando te plazca.

Lógicamente, el establecer siempre la Culture y UICulture para cada recurso de tu aplicación, también conlleva que no importa en que idioma esté instalado tu servidor, nunca serás dependiente en este aspecto.

En este punto alguien podría pensar que ocurrirá si el cliente que hoy es Culture en-US quiera cambiar mañana a Culture es-ES (por alguna decisión política que escapa a mi entendimiento). Pues bien, tenemos un problema y los señores de soporte (léase programadores) tendrían que convertir muchos datos antes de poder hacer este cambio, pero esta es otra historía…

Otra punto importante e íntimamente ligado con la internacionalización de aplicaciones, es el uso de las fechas UTC.

En España tenemos un ejemplo de diferencia horaria con las Islas Canarias. En USA es el caos, jamás se me ocurriría pedir la hora a alguien por la calle… Con este variopinto escenario, las decisiones que hemos tomado al respecto de las fechas en la aplicación son las siguientes:

  • Cualquier usuario de la aplicación tendrá asociado un uso horario, que además podrá cambiar en función de donde se encuentre físicamente (sería ya la repera que por geolocalización le pudiéramos ubicar o proponerle un cambio, todo llegará…)
  • Cada vez que se quiere obtener la fecha actual del sistema, se llamará a un método que calculará la fecha a partir de la fecha UTC actual del servidor y aplicará el uso horario del usuario. Finalmente, será la fecha calculada la que se grabe en el típico campo CreatedDate.

Para permitir al usuario cambiar su uso horario, podemos darle un desplegable con una lista de opciones obtenidas desde System.TimeZoneInfo.GetSystemTimeZones

clip_image001

Asumiendo que el usuario está en Madrid, grabaremos un campo asociado con el valor “Romance Standard Time” que es la propiedad Id de la clase TimeZoneInfo. Después y cuando queramos obtener la fecha actual del usuario (que no del servidor) ejecutaremos el siguiente método:

static DateTime GetCurrentDateTime(string timeZoneInfoId)

{

    var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneInfoId);

    return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, timeZoneInfo);

}

De esta forma siempre grabaremos en un campo del tipo CreatedDate la fecha actual del usuario según su uso horario y no según la hora del servidor.

Está claro que la internacionalización de aplicaciones es un mundo en sí mismo, pero por algún lado hay que empezar!

Un saludo!