Qoding: Internationalising custom Kentico modules


If you’ve spent any time with Kentico 8, you’ll probably know all about custom modules. They’re a powerful new feature of the CMS that allow quick and easy creation of a back end interface for content which doesn’t logically have its own page on the site.

If you’ve spent any time with Kentico 8, you’ll probably know all about custom modules. They’re a powerful new feature of the CMS that allow quick and easy creation of a back end interface for content which doesn’t logically have its own page on the site. It removes the need to write similar code over and over for different modules – such as the list page, edit form and database hook-up – and allows you to concentrate more on designing the specific custom functionality of that module.

I’ve used it to great effect on a number of projects (Interfleet, BM TRADA, NQA). However, a common theme of all those sites is that they are global – and unfortunately the current module builder doesn’t have an inbuilt way of dealing with this.

In this series of 3 articles, I will detail my approach to solving the issue of internationalising custom modules. Part 1 will deal with modifying a module class to enable content in multiple cultures and displaying it in the back end. In part 2, I’ll cover the issue of international permissions. And finally we’ll look at how to use this localised content on the front end.

I’ve assumed a certain level of knowledge of how to create custom Kentico modules, so we’ll not be covering that here. That has been dealt with excellently in the docs. I’ll also skim over a number of other areas which would add unnecessary bulk to this article and distract from the main point. I have tried to include links to the docs or other related blog posts though, in case you need to brush up on these parts of the CMS.

Part 1: Modifying the class

An example

I always find it really helps to have a concrete example to work with, rather than reading abstract bits of code. Let’s imagine we’re building a website for Ji’s Pies, a company selling delicious pastry-based goodness in a range of flavours. The website has a page listing all of the flavours of pie the company sells, but there is not a dedicated page for each.
 
ji-blog-1.png

Ji’s Pies has now decided to expand its operations to the USA, Sweden and Germany. Each will have its own version of the website with content relevant to that country and, where appropriate, translated into the local language.

In addition, they will not be selling all three flavours in each of these countries; they will instead be trialling one flavour in each region, along with a special new flavour not available elsewhere.

This requirement means that we can’t just use the “Translate field” checkbox on the title field of the PieFlavour class. We’ll need something smarter.

Adding a culture codes field

In the PieFlavour class, on the Fields tab, create a new field. Give it the name CultureCodes. It should be of type Text, and it should be required too. Give it a caption (“Cultures”) and set the form control to Multiple choice. (If content will only ever be applicable to one culture you can use a drop-down here.)

ji-blog-2.png

Now we need to get a list of cultures. These are stored in the CMS_Culture table. Under Data source you should select SQL Query and enter the following: 

SELECT CultureCode, CultureName
FROM CMS_Culture
WHERE CultureID IN (SELECT CultureID FROM CMS_SiteCulture WHERE SiteID={% SiteContext.CurrentSiteID |(user)Admin|(hash)b9b514b44f9481ce047d568b915fd172799d1040d0c492117e0b48f971c37cd7%})
ORDER BY CultureName


Lines 3 and 4 are not totally necessary, but they make the editing experience much nicer. On line 3 we’re ensuring only cultures enabled on this site are listed, and on line 4 we order them alphabetically. Not so relevant when we’ve only got 4 cultures, but imagine if Ji’s Pies expanded to 50 countries, or 100. Hey, I can dream!

Note that Kentico best practice would be to store this as a query. I won’t go into the details of this, but let’s assume we’ve stored it as Pies.PieFlavour.selectSiteCultures. We can now modify the SQL Query to use a macro:

{%Queries["Pies.PieFlavour.selectSiteCultures"].QueryText|(user)Admin|(hash)24159476590f6fa4904dfdb29b1087069ca203793add1faf3d05b87da13d46aa%}

ji-blog-3.png

Save the field and open up your Pie flavours interface. When you edit an existing flavour, you should now see a list of checkboxes – one for each culture.

Modifying the back-end list

Unfortunately, if we look at the list screen of the interface we don’t get to see which cultures are selected for each item. We need to modify the grid definition. We could just add the column using this:

<column source="CultureCodes" caption="Cultures" wrap="false" localize="true" />
 
But that doesn’t give us a particularly nice result. There are at least 3 problems:

1. We see the verbatim output from the database, which is a pipe-separated list of culture codes.

2. If we want to add a filter it’ll just be a text search.

3. We’re fixing the caption in English so if we wanted to add a multilingual UI it wouldn’t translate.

These are not functional aspects of our module. It will work perfectly well if we don’t solve these. But some real people in the marketing department at Ji’s Pies are going to have to use it – so let’s give them a good experience!

1. Showing a list of cultures

I’m going to assume you’re familiar with the concept of UI extenders (here’s a helpful guide) and that we already have one in place for your list of Pie flavours.

Let’s modify the OnExternalDataBound method:

private static object PieFlavourListOnExternalDataBound(object sender, string sourcename, object parameter)
    {
         switch (sourcename)
         {
             /* other case statements here
                ...
             */
             case "culturecodes":
                 return ((string) parameter).Split('|').Join(", ").ToLower();
         }
         return parameter;
     }


This would be a little nicer. We can do better though! How about this, which will show a list of culture names instead.

return ((string)parameter).Split('|').Select(c => CultureInfoProvider.GetCultureInfo(c).CultureName).Join("<br/>");

Much friendlier! Now we just need to hook it up to the grid by modifying our column definition:

<column source="CultureCodes" caption="Cultures" wrap="false" localize="true" externalsourcename="culturecodes" />

ji-blog-4.png

2. Adding a filter

This is a little trickier. What we ideally want is a drop-down of all the applicable culture codes. Thankfully, it’s really simple to add a custom filter to a UniGrid definition.

First, let’s create the filter itself. We need to create an ascx control that looks something like this:

<div class="form-horizontal form-filter">
    <div class="form-group">
        <div class="filter-form-condition-cell">
            <cms:CMSDropDownList ID="ddlCultureCodes" runat="server" CssClass="ExtraSmallDropDown" />
        </div>
    </div>
</div>


In the code behind, we want to inherit from CMSAbstractBaseFilterControl. I’ve not included the whole code here (see the zip file, link at end of article). The two main methods to worry about are GetCondition() and InitFilterDropDown(). 

public string GetCondition()
    {
        if (!string.IsNullOrEmpty(FilterCultureCode))
        {
            var culture = SqlHelper.EscapeQuotes(FilterCultureCode.ToLower());
            var whereCondition =
                string.Format("(CultureCodes = '{0}' OR CultureCodes LIKE '{0}|%' OR CultureCodes LIKE '%|{0}' OR CultureCodes LIKE '%|{0}|%')", culture);
            return whereCondition;
        }
        return string.Empty;
    }

 private void InitFilterDropDown(CMSDropDownList dropDownList)
    {
        if (dropDownList != null && dropDownList.Items.Count <= 0)
        {
            var parameters = new QueryDataParameters
                {
                    {"@SiteID", SiteContext.CurrentSiteID}
                };
            var cultures = new DataQuery("Pies.PieFlavour.selectSiteCulturesParameterised") { Parameters = parameters };
            ddlCultureCodes.DataSource = cultures.Execute();
            ddlCultureCodes.DataValueField = "CultureCode";
            ddlCultureCodes.DataTextField = "CultureName";
            ddlCultureCodes.DataBind();
            ddlCultureCodes.Items.Insert(0, new ListItem("All", ""));
        }
    }


A couple of things to point out here:

1. You might be wondering why I didn’t just use WHERE CultureCodes LIKE '%{0}%'. The reason is this could potentially bring up some false positives, since it’s possible for a culture code to be contained within another. A real example from a project I have worked on is a site with a custom en-int (International – English) culture as well as en-in (India – English). The simpler WHERE condition wouldn’t work as desired in this case – filtering by en-in would wrongly show en-int content.

2. I’ve used a new, parameterised version of the query, which replaces {% SiteContext.CurrentSiteID |(user)Admin|(hash)b9b514b44f9481ce047d568b915fd172799d1040d0c492117e0b48f971c37cd7%} with @SiteID.
I’ve saved this control in the CustomCMSFilters folder in the website project. Now let’s add it to the list:

<column source="CultureCodes" caption="Cultures" wrap="false" localize="true" externalsourcename="culturecodes">
    <filter type="custom" path="/CustomCMSFilters/CultureCodeFilter.ascx" source="CultureCodes" />
</column>


ji-blog-5.png

Don’t forget – the filter won’t show unless you’ve got more than a whole page of results, or you have set a custom filter limit.

3. Multilingual caption

This is the easy bit – just replace "Cultures" with "$General.Cultures$" and add entries to the relevant resource files.

Wrapping up

That’s it for part 1. You’ve now got a module which can handle creating content for use in different cultures! Next time, we’ll look at how to respect multilingual permissions.

19 May

2015
Ji Pattison-Smith
Listed in:  Development Platforms
Estimated read time:
 words,  minutes

Signup to receive these articles straight to your inbox.