Localizing files (*.jpg, *.css, *.js, etc) in ASP.NET

Localizing FilesWell, I ran into this challenge today and figure I would share it.

When initially browsing for an already-made solution I found people that would create a new Helper Method (in ASP.NET MVC) to generate localized URLs for content, but I just didn’t like this approach. Just for the record, here’s a link of where I read about such approach.

The reasons I don’t like the helper method approach are:

  • SEO-wise (not that SEO cares about stylesheets or javascript files), you’ll be generating different URLs on the fly for what is essentially the very same content except for the localization. This is, to me, simply a conceptual miss.
  • Development wise, you’ll endup with either an Url.Content method overload or a totally new method to create the URL for you, but you’ll bet at the mercy of your developers remembering to use the “localized” version of your URL generator (this is all thinking of an ASP.NET MVC environment).

So, I came up with this approach:

The Concept

Basically, instead of localizing when the server generates the URL, why not localize when the server receives the request for the actual resource file (be that what you need to serve… JPG or PNG images? Javascript files? CSS?).

Before you start working on this, you should probably understand that this is not an internationalization approach for your site. If you need to internationalize/localize your dynamic content, then you can start by looking at these 2 posts (post 1post 2). We use a cookie to identify the culture selected by the user… and that is how the following code will serve localized content.

So, what we’ll do here, is:

  1. Trap requests to static files
  2. Figure out if a localized version of the file exists (in our case, if a file with the same name in a subfolder with the culture exists)
  3. If the localized file exists, then serve it.
  4. If the localized file doesn’t exist, then server the original file.

The Code

We’ll need an HttpHandler… to handle the requests.

[file LocalizedStaticFileHandler.cs]

 public class LocalizedStaticFileHandler : IHttpHandler
{
    protected static List<FileSystemCheckEntry> FileSystemCheckCache = new List<FileSystemCheckEntry>();
    protected const int MAX_FILE_SYSTEM_CHECK_CACHE_SIZE = 100;
    /// <summary>
    /// You will need to configure this handler in the web.config file of your
    /// web and register it with IIS before being able to use it. For more information
    /// see the following link: https://go.microsoft.com/?linkid=8101007
    /// </summary>
    #region IHttpHandler Members
    public bool IsReusable
    {
        // Return false in case your Managed Handler cannot be reused for another request.
        // Usually this would be false in case you have some state information preserved per request.
        get { return true; }
    }
    public void ProcessRequest(HttpContext context)
    {
        // first, let's get the path to the file that is being requested
        var filePath = context.Server.MapPath(context.Request.Url.AbsolutePath);
        // let's check out the cookie, and see if we are supposed to look for a localized version of the file
        var locale = CultureHelper.GetDefaultCulture();
        var localeCookie = context.Request.Cookies[CultureHelper.CULTURE_COOKIE_NAME];
        if (localeCookie != null)
            locale = CultureHelper.GetImplementedCulture(localeCookie.Value);
        // see if the localized file exists
        var localFilePath = Path.Combine(Path.GetDirectoryName(filePath), locale, Path.GetFileName(filePath));
        // set output cache policy (see Global.asax::GetVaryByCustomString)
        context.Response.Cache.SetVaryByCustom("language");
        // if the file exists, then server the file
        if (this.FileSystemCheck(localFilePath))
        {
            this.ServeContent(localFilePath, context);
            return;
        }
        // if doesn't exist, transmit the requested file
        else if (this.FileSystemCheck(filePath))
        {
            this.ServeContent(filePath, context);
            return;
        }
        // if the file doesn't exist, then return a 404 error
        context.Response.StatusCode = 404;
    }
    protected bool FileSystemCheck(string filePath)
    {
        // see if the entry already exists
        FileSystemCheckEntry entry;
        lock (LocalizedStaticFileHandler.FileSystemCheckCache)
        {
            entry = LocalizedStaticFileHandler.FileSystemCheckCache.FirstOrDefault(x => x.FilePath == filePath);
            if (entry != null)
            {
                // since it was found, let's move to the top!
                LocalizedStaticFileHandler.FileSystemCheckCache.Remove(entry);
                LocalizedStaticFileHandler.FileSystemCheckCache.Insert(0, entry);
                return entry.Exists;
            }
        }
        // if it doesn't exist, then let's actually check for the file
        var exists = File.Exists(filePath);
        // now insert at the top and get rid of overflow
        lock (LocalizedStaticFileHandler.FileSystemCheckCache)
        {
            LocalizedStaticFileHandler.FileSystemCheckCache.Insert(0, new FileSystemCheckEntry { FilePath = filePath, Exists = exists });
            while (LocalizedStaticFileHandler.FileSystemCheckCache.Count > LocalizedStaticFileHandler.MAX_FILE_SYSTEM_CHECK_CACHE_SIZE)
                LocalizedStaticFileHandler.FileSystemCheckCache.RemoveAt(LocalizedStaticFileHandler.MAX_FILE_SYSTEM_CHECK_CACHE_SIZE);
        }
        return exists;
    }
    protected void ServeContent(string filePath, HttpContext context)
    {
        // clear current
        context.Response.Clear();
        context.Response.ClearContent();
        // set the MIME type
        switch (Path.GetExtension(filePath).ToLower())
        {
            case ".jpg":
            case ".jpeg":
                context.Response.ContentType = "image/jpeg";
                break;
            case ".gif":
                context.Response.ContentType = "image/gif";
                break;
            case ".png":
                context.Response.ContentType = "image/png";
                break;
            case ".css":
                context.Response.ContentType = "text/css";
                break;
            case ".js":
                context.Response.ContentType = "text/javascript";
                break;
        }
        context.Response.WriteFile(filePath);
    }
    #endregion
    protected class FileSystemCheckEntry
    {
        public string FilePath { get; set; }
        public bool Exists { get; set; }
    }
}

And also add the following to your Global.asax.cs file (to enable output caching based on the Culture Cookie):

1
2
3
4
5
6
7
8
9
10
11
12
public override string GetVaryByCustomString(System.Web.HttpContext context, string custom)
{
    if (custom == "language")
    {
        var locale = CultureHelper.GetDefaultCulture();
        var localeCookie = context.Request.Cookies[CultureHelper.CULTURE_COOKIE_NAME];
        if (localeCookie != null)
            locale = CultureHelper.GetImplementedCulture(localeCookie.Value);
        return locale;
    }
    return base.GetVaryByCustomString(context, custom);
}

You’ll note that this handler depends on a CultureHelper, as suggested in Nadeem’s post (so I won’t post the code here).

You can also note that we are using a FileSystemCheckCache. This is to avoid asking the File System everytime we need to figure out if a localized file exists. This can be a heave burden if you use UI frameworks with lots and lots of javascript and sprites.

So now, let’s enable the Handler on your web.config.

Put this inside your web.config’s configuration/system.web/httpHandlers tag:

1
2
3
4
5
6
7
8
<httpHandlers>
  <add verb="GET" path="*.gif" type="YourWebSite.Handlers.LocalizedStaticFileHandler" />
  <add verb="GET" path="*.jpeg" type="YourWebSite.Handlers.LocalizedStaticFileHandler" />
  <add verb="GET" path="*.jpg" type="YourWebSite.Handlers.LocalizedStaticFileHandler" />
  <add verb="GET" path="*.png" type="YourWebSite.Handlers.LocalizedStaticFileHandler" />
  <add verb="GET" path="*.css" type="YourWebSite.Handlers.LocalizedStaticFileHandler" />
  <add verb="GET" path="*.js" type="YourWebSite.Handlers.LocalizedStaticFileHandler" />
</httpHandlers>

And if you use IIS7 or above, then you’ll need to put the following within yourconfiguration/system.webServer/handlers tag:

1
2
3
4
5
6
7
<handlers><br>  <add name="LocaleGIF" verb="GET" path="*.gif" type="YourWebSite.Handlers.LocalizedStaticFileHandler" preCondition="integratedMode" />
  <add name="LocaleJPEG" verb="GET" path="*.jpeg" type="YourWebSite.Handlers.LocalizedStaticFileHandler" preCondition="integratedMode" />
  <add name="LocaleJPG" verb="GET" path="*.jpg" type="YourWebSite.Handlers.LocalizedStaticFileHandler" preCondition="integratedMode" />
  <add name="LocalePNG" verb="GET" path="*.png" type="YourWebSite.Handlers.LocalizedStaticFileHandler" preCondition="integratedMode" />
  <add name="LocaleCSS" verb="GET" path="*.css" type="YourWebSite.Handlers.LocalizedStaticFileHandler" preCondition="integratedMode" />
  <add name="LocaleJS" verb="GET" path="*.js" type="YourWebSite.Handlers.LocalizedStaticFileHandler" preCondition="integratedMode" />
</handlers>

and voila! You should now be able to just create localized versions of your files within a culture-specific subfolder.

- ~/Content/
  +-- images/
      +-- es-ES/
          +-- logo.png
      +-- logo.png

The Pros

  • You can continue to use standard Url.Content calls to generate content (or whatever approach you were already using for your ASP.NET application). Your developers won’t have to remember to use the localized version of your URL generators.
  • From a strict content perspective, you are reflecting the fact that you are serving the very same resource, only with localization tweaks. This means it’s your same company’s logo… just with a little subtitle in a different language.

The Cons

  • You may face a slight performance impact since we are checking for every single static file you serve if there is a localized version of it. You could apply the handler to specific subpaths or play with the Cache size to overcome this.
  • The Cache implementation is a little rough, and lots can be improved on this sense.

What does “Senior Developer” mean?

Senior Software DeveloperI gave myself the task of determining, for our organization, what does “Senior Developer” mean? Really there’s no absolute definition for it, but I lay here a set of aspects and what expectations are there for a Senior…

How much experience must a Senior have?

This is one of the most confusing parts of defining a Senior. Many organizations will tend to name you “Senior” just because you have 8+ or 5+ years of development experience… This is, of course, not accurate enough. One can argue that if he/she has  8+ years of experience, then one can assume the Senior Developer has faced several problems, has owned/designed/developed several projects or whatever.

I have personally worked with people with 5+ years of experience and have not proven to be the Senior I expected them to be… you’ll have to keep reading to know what areas they might’ve been found short, but the bottom line is that Years of Experience means nothing unless it was within your organization, where you can “guess” (if not know) what kind of challenges and what business has the developer been facing and working on.

On what context are you Senior?

Really, there shouldn’t be a context as long as we are talking about Software Development, but I’ve seen people go back and forth with this idea. For instance, If you are a Senior Developer in .NET Technologies, does that mean you are a Junior in LAMP? or Java? Bottom line is that, technically, yes. But what about behavioraly?

If you are a Senior, you know of Best Practices and Patterns of Problem Solving and Automation, regardless of the programming languange!! (which is why most Software Developers tend to build better Excel macros than regular people :)). Of course you will have the challenge of learning the tools and actual implementations if you switch language, but you are the very same Developer behind the language.

Some state that as long as you are a Developer on more than 1 language, you are on your way to be a Senior… makes sense!

What and How a Senior asks?

This is slightly related to the next title, and considers the simple fact that:

  • If you are a Junior and I give you a Development task, you’ll come back asking: “Where is the code for that? Will that be a change on the Web Frontend or the Backend?”
  • If you are a Senior and I give you the very same task, you’ll come back asking: “Is that code on TFS or SVN (cause I couldn’t find it anywhere)? Is it ‘Project 1’ or ‘Project 2’? I opened that solution and it seems the change should go in the Data Access layer of the Backend Service, is that right?”

Now, the important thing here is not the fact that someone can read code and recognize… what’s really important here is “How much of my time will it get you up and running”? (again, related to the title below). So my expectation from a Senior Software Developer is that he/she asks the right questions, and not just machine fires away.

How self-sufficient a Senior is?

This relates to how a Senior gets by with a task in hand?

  • Will he/she need constant supervision? or can we trust him/her to actually supervise others?
  • Can we trust the Senior Developer to make decisions about the system’s architecture?
  • Can we trust the Senior Developer will know who the right person is to clarify any unknown or blurry detail?
  • Will I need to write a 4 page spec for a button’s behavior change? or can I just state it in 1 sentence and the developer will ask the relevant and take what he can from current code base, peers and documentation?

This is, I believe, the most important aspect of a Senior Developer… you need to be able to trust their judgement and that they know your organization well enough they will make the right decisions and represent your ideals in an accurate way.

How a Senior communicates?

This is one of the most uncommon softskill amongst Software Developers. We talk “bits” with our computers, but we talk “biz” with our clients. It’s really important in the Software Development business not only to understand the business of our customers, but actually be able to speak to them in their business terms (so they understand us).

Even though a Senior should be Expert on certain given technologies, I NEVER expect a Senior to speak in those terms with a non-technical client. I expect a Senior to be able to translate technologies into benefits and costs, and to be able to orient customers into making the most technically-wise decision.

In addition to biz talk, we need our people to be able to communicate with any other non-client human being as well. Will they write an email that goes round and never gets to the point? Will they talk to you using the wrong terms for your context (i.e. calling “users” to what our app knows as “web clients”)? There are a lot of Communication Skills that are invaluable to a Team’s productivity, and Senior Developers are supossed to excel at this (if not all).

How a Senior owns his/her work?

It really won’t matter if you hire the most knowledgeable person in the world on a specific technology, if that person doesn’t care that your system ends up right! I don’t mean “Own” as in “have 1 developer own the UI and another developer own the backend”… it’s more like “does my developer care that we provide an scalable solution?” and alike.

I expect a Senior to be proactive, and look for ways to improve anything! If you assign a Developer to work on an old piece of code, you should at least get a “why are we still doing this if there are other newer/better ways of doing it?” comment/question. It’s that sense that allows us to think that a Senior Developer will come up with a good improvement for our system, even though there’s no business requirement that demands for it just yet (like upgrading to MVC web pattern, or to new versions of whatever Framework/Technology you develop on).

How a Senior abstracts his/her work?

This has to do with the Senior’s ability to see the Big Picture. It really goes from the simple “if I change this, will it affect compilation on other projects/components?” to the “will changing this behavior impact the business performance of my client in a negative way?” or “will changing this logic prevent my client from developing a new business opportunity?“.

This skill on a Senior Developer is VERY valuable, cause it allows him/her to think outside of the code. It allows a Senior Developer to think in business terms, and also think that we are not alone in a project. I’ve found myself fixing bugs introduced by other developers (fixing other bugs) too many times now… and I don’t really expect that to ever go away, but the chances of a Senior introducing that are much lower than a Junior (even when changing a new system they are getting ramped up on).

 

To Summarize

So, in the end, being a Senior Developer is much more than knowing by hart the steps to configure a Web Service in Java, or knowing what the caveats of implementing an ADO.NET Entity Framework 3.5 on a Web Solution are… it’s really all about how mature are you as a Software Developer, regardless of the Programming Language. If you hire a Senior Developer, you should expect to still have to train them in your organizational culture, in what technologies you use and what systems are already there… but you should rest assured that a Senior will make good decisions for your business and your systems.