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;
}
}
}
}