Multilingual BlogEngine.NET code behind, part 2

Currently rated 4.0 by 3 people

  • Currently 4/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
March 28, 2009 16:47 by Jaroslav Kobližek

This is a second part of my guide through multilanguage support of Enhanced BlogEngine.NET. In the previous part we've taken look at SQL data storage, XML data storage and how Guid becomes KeyCulturePair<Guid>.

Accessing the front-end cultures collection

When the application starts we need to store the list of front-end cultures somewhere. The best place is in BlogEngine.Core.BlogSettings class, because this list is also about application settings and we need access to it whenever we need. The list of front-end cultures is placed into a slightly extended BlogEngine.Core.CultureCollection class (some indexers are added) that inherits from System.Collections.Generic.List<CultureCollectionItem>.
CultureCollectionItem is the class we talked above. We use it in CultureCollection and KeyCulturePair classes.

BlogEngine.NET saves all properties in BlogSettings class no matter what. If we add a property called FrontendCultures that returns our CultureCollection, the application will save it to data storage as well. But, it doesn't save it as a list of cultures, but only a string "BlogEngine.Core.CultureCollection". To prevent saving such useless settings, there's a new attribute class BlogEngine.Core.NoSaveSettingsAttribute. If the property is marked with this attribute, the application will skip it when saving all settings.

URL Rewrite

The most important change along side with SQL and XML providers has been made in BlogEngine.Core.Web.HttpModules.UrlRewrite class. But first, the method Application_PreRequestHandlerExecute() has been removed from global.asax and the content moved to UrlRewrite class.

UrlRewrite class has been extended in several ways.

  • Thread culture is set based on known information in the following order:
    • If URL contains corresponds to the administration area, thread culture will be set from BlogSettings.Instance.Culture value. This is a value from the combo box in the Settings tab of administration area.
    • If URL contains culture name (cs, fr, en,…), it will be set based on this piece of information.
    • Checks whether the user has a cookie with culture information. If yes and the application supports it, it will be set based on the cookie.
    • If the user does not have any cookie set, the application checks the browser language preference. If there are cultures that matches the application's supported list, it will be set based on the browser language preference.
    • If all fails, the application will set the first active language on the list. This may most likely happen to web crawlers or other bots, when they don't have the URL that contains culture name.

private void SetThreadCulture(HttpContext context) // HACK: Multilanguage
{
    string path = context.Request.RawUrl.ToUpperInvariant();

    if (context.Request.QueryString.ToString() == "setculture")
    {
        HttpCookie cookie = context.Request.Cookies["frontend_language"];
        if (cookie == null)
        {
            cookie = new HttpCookie("frontend_language");
        }
        if (Regex.IsMatch(context.Request.RawUrl, "^" + Utils.RelativeWebRoot + "[a-z]{2}(-[a-z]{2})?/", RegexOptions.IgnoreCase))
            cookie["culture"] = Regex.Match(context.Request.RawUrl, "^" + Utils.RelativeWebRoot + "([a-z]{2}(-[a-z]{2})?)/", RegexOptions.IgnoreCase).Groups[1].Value.Trim('/');
        cookie.Expires = DateTime.Now.AddYears(3);
        context.Response.Cookies.Add(cookie);
    }

    if (path.Contains("/ADMIN/"))
    {
        if (!string.IsNullOrEmpty(BlogSettings.Instance.Culture))
        {
            if (!BlogSettings.Instance.Culture.Equals("Auto"))
            {
                SetCulture(BlogSettings.Instance.Culture);
            }
        }
    }
    else if (Regex.IsMatch(path, "^" + Utils.RelativeWebRoot.ToUpperInvariant() + "[A-Z]{2}(-[A-Z]{2})?/"))
    {
        SetCulture(Regex.Match(path, "^" + Utils.RelativeWebRoot.ToUpperInvariant() + "([A-Z]{2}(-[A-Z]{2})?)/").Groups[1].Value.Trim('/'));
    }
    else
    {
        HttpCookie cookie = context.Request.Cookies["frontend_language"];

        if (cookie != null)
        {
            SetCulture(cookie["culture"]);
        }
        else if (context.Request.UserLanguages != null && context.Request.UserLanguages.Length > 0)
        {
            Regex languageMatch = new Regex("[A-Z]{2}(-[A-Z]{2})?", RegexOptions.IgnoreCase);

            foreach (string userLanguage in context.Request.UserLanguages)
            {
                if (BlogSettings.Instance.CultureFrontend[languageMatch.Match(userLanguage).Value] != null)
                {
                    SetCulture(languageMatch.Match(userLanguage).Value);
                    cookie = new HttpCookie("frontend_language");
                    cookie["culture"] = languageMatch.Match(userLanguage).Value;
                    cookie.Expires = DateTime.Now.AddYears(3);
                    context.Response.Cookies.Add(cookie);
                    break;
                }
            }
        }
        else
        {
            foreach (CultureCollectionItem item in BlogSettings.Instance.CultureFrontend)
            {
                if (item.Active)
                {
                    SetCulture(item.CultureName);
                    cookie = new HttpCookie("frontend_language");
                    cookie["culture"] = item.CultureName;
                    cookie.Expires = DateTime.Now.AddYears(3);
                    context.Response.Cookies.Add(cookie);
                    break;
                }
            }
        }
    }
}


You are maybe curious, where is Session? Well, the original BlogEngine.NET does not use it, so do the enhanced version. In most cases, the SessionID is saved to some cookie anyway and the culture information is not security threatening.

Another extended part of the UrlRewrite class is in the URL checking. It must divide the URL into three groups. The first group are culture invariant URLs. The second group are URLs without culture name that must be redirected to the URL with culture name information. And the final group are URLs with culture name information that are rewritten to specific path.

Culture specific HttpHandlers are syndication.axd, rsd.axd, blogml.axd, sioc.axd and trackback.axd + blogimporter web service. Other handlers are culture invariant.

Replacing challenge

The final part is about checking all classes.

  1. If the Id property is presented, it will be changed to Id.Key and the culture must be taken into consideration as well.
  2. If the code binds posts, comments or other similar things, it will be replaced to filter culture specific content only. Localized posts for the current culture can be retrieved from BlogEngine.Core.Post.LocalizedPosts property. Same for Page class with LocalizedPages property, or Categories class with LocalizedCategories property.
  3. All links must contain URLs with current culture name, except for culture invariant URLs (admin, file.axd, image.axd,…). Current culture name can be retrieved from BlogEngine.Core.Utils.CurrentLanguage property.
  4. ...and other changes + testing.

I hope you all got a simple picture of how it all works. If you have any questions, leave a comment or contact me.


Comments