Thursday, September 28, 2006

The Battle with Asp.Net 2.0 Implicit Localization and Manual Selection of Language

If you have tried to Localize your Asp.Net 2.0 sites, you may have run into a problem with Implicit Localization. It works great whenever the user sets their language settings in the browser, but if you try to provide a choice on your site for the language, it does not work at all.

While I was researching the problem, I read a few different sources that said it was not possible to use implicit localization and allow the user to manually select a language. Obviously this did not set well with me. First, why would Microsoft make implicit localization work so well, if you cannot use it. Even MSDN recommends that you provide a drop down so the user can choose the language. However, you as soon as you do this, the resource files are ignored and you must use explicit localization everywhere. This means you will have to inject ugly code all over your aspx page.

However, I also found a video that gave me hope. In the video, the speaker was able to use implicit localization without a problem. So, with confidence that the articles I had read were wrong, I followed the video. However, my results were not the same. In the video, everything changed correctly. With my site, it refused to work. The only way I can explain it, is that the video was using a beta of .net and not the final release.

However, I was not going to accept this. Surely there must be a way to make this work. It was time for the OOP in me to step aside, I was bringing out Reflector.

First thing I examined was the HttpContext.Current.Request object in the VS debugger. And what did I find: a Headers property, with a setting that I could identify as my rogue browser language setting. Using the power of reflection I ripped that private field out of its cozy OOP lair and stuck in my test value "en". "Yes! I have found the solution, I am going to write a blog about this," I thought. However, as my page loaded, there I stared the French in the face.

"No! The Localization must not use that setting... or maybe the Pre_Init is not early enough..." Well, I was about to accept defeat, when I glanced another variable in my Request: UserLanguages. "Hmm, there is another, I will take care of that." After another battle using Reflection, I had smitted the variable upon the rocks. However, upon testing nothing had changed. "There must be more places that value is stored." So I continued to search and I found something I did not expect: An HttpWorkerRequest. "Surely, this is the source of my enemy." After spending some time fighting with this using Reflector as my light and Reflection as my sword, I had eradicated the source of the browser setting and reset everywhere it was stored in Request. "This has been a long battle friend, but finally you are defeated." However, much to my surprise, my enemy was still alive and attacked me from behind. I was defeated. Somehow, somewhere that value had already hidden a copy of itself where I could not find it. I retreated and went to bed...with a lingering thought in my mind, "It must be too late."

In the morning, a thought revealed itself that had been manifesting in my dreams. "The Pre_Init is too late, maybe Global.asax." Consolting in my trusted and wisest collegue Google, I soon discovered some events that could help me. The first event, proved to be too late by far. The second, however was successful. Using the same weapon I had granted Pre_Init, he was able to stealthily replace the browsers reported language setting with our own... But would it be successful... Yes! English was back, our test value had successfully overrwritten any remnant of the browser setting. Victory is ours! The ring was destroyed and all of Mordor's powers allong with it.

All we needed to do now was use the drop down list setting instead of the test value. We could get the value of the drop down list from the http post headers, and so we did. Another successfuly test. But when we changed pages, a an servant of the dark setting attacked us. The drop down list did not retain its value whenever we changed pages. However, SessionState ran him off after changing to a alternate event in Global.asax.


The following are the final cronicals as taken by VS:

Default.Master:
A DropDownList named ddlLanguage
With choices:
Text="Auto" Value=""
Text="English" Value="en"
Text="Francais" Value="fr"
And a OnSelectionChanged event handler (see below)

Default.Master.cs:

protected void Page_Load( object sender, EventArgs e )
{
if( !IsPostBack )
{
ddlLanguage.SelectedValue = LanguageHeader.SessionLanguage;
}
}

protected void ddlLanguage_SelectedIndexChanged( object sender, EventArgs e )
{
LanguageHeader.SessionLanguage = ddlLanguage.SelectedValue;
}

Global_asax:

void Application_PreRequestHandlerExecute( object sender, EventArgs e )
{
LanguageHeader.SetLanguageHeader();
}


LanguageHeader.cs:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

using System.Threading;
using System.Globalization;
using System.Collections.Specialized;
using System.Reflection;

public class LanguageHeader
{

public static void SetLanguageHeader()
{
string language = GetLanguage();

if( string.IsNullOrEmpty( language ) )
{
return;
}

HttpRequest request = HttpContext.Current.Request;
NameValueCollection headers = request.Headers as NameValueCollection;

//MethodInfo methodMakeReadWrite = headers.GetType().GetMethod( "MakeReadWrite", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
//MethodInfo methodMakeReadOnly = headers.GetType().GetMethod( "MakeReadOnly", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );

//MethodInfo methodFillInHeadersCollection = request.GetType().GetMethod( "FillInHeadersCollection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
//MethodInfo methodFillInServerVariablesCollection = request.GetType().GetMethod( "FillInHeadersCollection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
//MethodInfo methodFillInParamsCollection = request.GetType().GetMethod( "FillInHeadersCollection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );

FieldInfo fieldHeaders = request.GetType().GetField( "_headers", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
FieldInfo fieldServerVariables = request.GetType().GetField( "_serverVariables", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
FieldInfo fieldParams = request.GetType().GetField( "_params", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );

FieldInfo fieldUserLanguages = request.GetType().GetField( "_userLanguages", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
FieldInfo fieldWr = request.GetType().GetField( "_wr", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );

object wr = fieldWr.GetValue( request );
FieldInfo fieldKnownHeaders = wr.GetType().GetField( "_knownRequestHeaders", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
FieldInfo fieldAllRawHeaders = wr.GetType().GetField( "_allRawHeaders", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
string allRawHeaders = fieldAllRawHeaders.GetValue( wr ) as string;
string[] knownHeaders = fieldKnownHeaders.GetValue( wr ) as string[];


//methodMakeReadWrite.Invoke( headers, null );

string before = headers[ "Accept-Language" ];
string[] beforeUserLanguages = HttpContext.Current.Request.UserLanguages;

string newRawHeaders = allRawHeaders.Replace( "Accept-Language: ", "Accept-Language: " + language + ";" );
fieldAllRawHeaders.SetValue( wr, newRawHeaders );

fieldUserLanguages.SetValue( HttpContext.Current.Request, new string[] { language } );
knownHeaders[ 23 ] = language;

fieldHeaders.SetValue( request, null );
fieldServerVariables.SetValue( request, null );
fieldParams.SetValue( request, null );

//string after = HttpContext.Current.Request.Headers[ "Accept-Language" ];
//string[] afterUserLanguages = HttpContext.Current.Request.UserLanguages;

//methodMakeReadOnly.Invoke( headers, null );
}

private static string GetLanguage()
{
string language = HttpContext.Current.Request[ "ctl00$ddlLanguage" ];

if( language == null )
{
language = SessionLanguage;
}

return language;
}

public static string SessionLanguage
{
get
{
if( HttpContext.Current.Session != null )
{
return HttpContext.Current.Session[ "Language" ] as string;
}
else
{
return null;
}
}
set
{
if( HttpContext.Current.Session != null )
{
HttpContext.Current.Session[ "Language" ] = value;
}
}
}
}

Thursday, September 21, 2006

Logic Recognition

For the past two years, I have been teaching C# to blind students. One of the major pains of the internet is image recognition systems. If a site uses image recognition without any alternative, then a blind person must contact tech support, etc. This turns their registration into a week worth of figthing with tech support before they get an account.

Sites that do offer an alternative should be commended, but there is still a problem. Sound recognition is often used as an alternative. However, these are sometimes very difficult to understand. Imagine a person with both vision and hearing loss, it remains difficult and sometimes impossible.

Also, bots are increasing their ability to deceifer the images and sounds and will eventually solve them just as well as a human.

I have thought of a better alternative: Logic recognition.
Basically, create simple logic puzzles that must be completed by the user.
Example:

Enter the third word of the following sentance:
I like to eat eggs in the morning.
Answer: [to]


Obviously this could easily be solved if a bot could be programmed to solve this type of problem. The solution to this is variety: problem types, randomness in the problem, using abbreviations, incorrect grammer, typos, etc.

For example (If you wanted to make it really hard for a bot):

Enter TheWordAfterThe 5th Word of the Following Sentance:
If you are a bot you will enter 'bot', not 'you'.

If a bot were designed for the first type of problem, it would think it should use the 5th word. We actually want the 6th word in this problem.


Another type of problem:

Sort the following words in alphabetical order. Make sure to leave a space between each word:

dog
cat
apple

Answer: [apple cat dog]


Again this is easy for a bot to solve, let's make it harder.

Sort the following words in alphabetical order. Make sure to leave a space between each word. Also, leave out the animal that meows:

dog
cat
apple

Answer: [apple dog]


Image and Vocal recognition are simple compared to the variety of logic puzzles you can create.


Another great benefit of this, is that the problem generation is processor fiendly. It is simple string manipulation. On the other hand, image and sound generation would need a dedicated server for large sites.

Also, the sentances and words used in the puzzles can be placed in an xml document. The xml document can be customized for the site to include slogans and other advertizing propaganda.


I hope this will become widely used. We are going to implement this on a site for which I program. I will add a link when it is done.