AJ's blog

April 19, 2014

ASP.NET MVC I18n – Part 10: Finish

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 4:09 pm

This is it. The complete overview of localizing a regular MVC based web application.

Here’s what we covered in this series, shown in context. The image contains links to the respective posts:


Quite some content for a rather common demand…

 

….and still, there’s some ground I did not cover:

right-to-left.

This is the one thing regarding localization I avoided. The reason is simple: I don‘t know enough about this topic.

Technically there is the dir attribute in HTML and a respective direction property in CSS. But beyond that I would also want to know how RTL affects the screen layout, tab order, etc..

WebAPI.

Not much to say. WebAPI controllers participate in the scheme we have set up, so the controller is aware what culture the user prefers. The data serialization itself ignores the culture, thus data exchange at the raw level (JSON) is not even affected.

Client side localization.

Apart from a few hints regarding client side validation (which is specifically supported by MVC via unobtrusive validation), everything I presented basically happened on the server.

So, if you build your application using Knockout, Angular, Sencha, or other frameworks – which generate large chunks of the UI employing databinding strategies – you will need to localize on the client as well. The server may still provide the necessary backing in terms of logic and translations e.g. as JSON service.

And JavaScript itself is still evolving.

“App” development.

Taking „client side“ one step further and building container applications based on PhoneGrap/Cordova, you will have to provide everything on the client; you just cannot afford to not render the UI just because the device has no connection right now. Thus you’ll need to address localization completely on the client, functionality as well as information; no server backing in this case, sorry.

Data localization.

For an application used internationally, it may not be sufficient to just localize the UI. You may have to localize your actual data. Formats, content, even business rules.

This may include providing downloadable content in different languages. (That exposé just became a collection, rather than a single file.) Take the different naming schemes into account. International addresses? Have fun. Types of business entities. Currencies, calendars, other units of measurement. (Thank god, we‘re talking about business applications; historians have to deal with the really funny stuff… . (Some people actually think IBAN and BIC are simple… .)

 

Well, I’m going to leave these topics for someone else. And with that, I conclude this little series. You can find the sources of the sample I built for this series here (VS2013 project, NuGet packages have to be restored using “Enable NuGet Package Restore”, IIS Express is expected).

 

That’s all for now folks,
AJ.NET

February 1, 2014

ASP.NET MVC I18n – Part 9: 3rd Party Controls

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — Tags: — ajdotnet @ 4:48 pm

Localization quite regularly involves localizing additional 3rd party controls. This is something that your control has – obviously – to support. Fortunately many controls – certainly the commercial ones, such as Telerik do. Unfortunately (but not unexpectedly) each and every one has its own approach to localization.

Note: This post is part of a series

To get a better feeling for the demands, let’s take a look at the jQuery UI date picker – the canonical example for additional controls.

Using the date picker is relatively simple: It typically is already included in an MVC project, the respective bundles for script and CSS files are readily defined and only need to be included in the _layout.cshtml. That done, a little initialization script (datepicker.initialize.js) will turn a textbox into a date picker:

$(document).ready(function () {
    var lang = $("html").attr("lang");
    $("input[type='date']").datepicker();
});

This script needs to be included in our views which can be achieved with minimal impact by including it in the existing UI bundle:

var bundle = bundles.GetBundleFor("~/bundles/jqueryui");
bundle.Include("~/Scripts/datepicker.initialize.js");

And the result works as expected…

… using American localization.

 

Localization

In order to localize the date picker, you need respective localization files. You can either grab them all (quite simple using the nuget package jQuery.UI.i18n), or you can grab the ones you actually need from github. Using these files, again, causes minimal impact if you simple include them in the bundle as above:

var bundle = bundles.GetBundleFor("~/bundles/jqueryui");
bundle.Include(
    "~/Scripts/jquery-ui-i18n.js", // option 1: from nuget package
    //"~/Scripts/jquery.ui.datepicker-*", // option 2: single localization files

    "~/Scripts/datepicker.initialize.js");

Including the localization scripts will not only register the necessary information, it will actually switch the default language for the date picker. So, if you grabbed the file using nuget, you will end up with Chinese (zh-TW) as date picker region…

With single files, you might include just the one that is actually needed, and avoid that issue. Only you can’t use the nuget package in this cases, as it contains one single script file with all localizations. In any case you could provide your own initialization, that comes after those scripts and sets the date picker region to the correct one, by adjusting the initialization script above:

var lang = $(„html“).attr(„lang“);
$.datepicker.setDefaults($.datepicker.regional[lang]);

We have all the information handy and did the same thing for globalize, right?

Wrong actually. Date picker localizations generally exist for the neutral culture (e.g. de) and only those specific cultures that differ form the default one. E.g. there is a localized version for en-GB, but not en-US, or de-DE respectively, as en and de are sufficient. On top of that the date picker does not support a proper fallback strategy, i.e. setting the region to de-DE will end up using the last included region, again zh-TW if you included all regions, not de.

This is an issue that is rarely mentioned and sometimes missed completely.

The solution is a mapping from our application‘s supported regions to regions supported by the date picker. You could do this on the server and provide a second attribute (e.g. langDatePicker) beside lang on the html tag. If you grabbed single localization files for the date picker, you could automate this by just scanning the available .js files. Or – for just two regions probably the more sensible approach – just do it in the initialization script:

$(document).ready(function () {
    var lang = $("html").attr("lang");
    
    if (lang == "de-DE")
        $.datepicker.setDefaults($.datepicker.regional["de"]);
    else
        $.datepicker.setDefaults($.datepicker.regional[""]); // en-US is default
    
    $("input[type='date']").datepicker();
});

And now the date picker changes formats, appearance (e.g. first day of the week), etc.:

 

Verdict

Localizing the date picker control can e done completely in all aspects. But there are also some inconsistencies compared to localizing the validation:

  • The globalize nuget package comes with single files, the date picker nuget package provides one consolidated file. This makes it harder to include just the one localization needed. (Performance is hardly an argument, as this is addressed by the bundle logic anyway.)
  • With Globalize you need to set a region explicitly, the date picker does it implicitly with referencing the localization file.
  • Globalize supports a fallback to the neutral culture if the specific region is not available. Date picker does nothing of the kind and simply ignores the call.

And these are the subtle differences in two packages from the jQuery universe. That might be a good indicator as to what to expect with 3rd party libraries from another source. Bring together a couple of controls, and you’ll have a mighty mishmash.

 

That’s all for now folks,
AJ.NET

January 25, 2014

ASP.NET MVC I18n – Part 8: Data Validation

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 3:13 pm

The last post showed how to display localized validation error messages. Time to be able to actually validate ;-)

Note: This post is part of a series

This is another topic that is widely documented for the most part, but has some notable gaps.

Remember the error from last post:

Having the error message in localized German does not solve the fact that the date is actually in the valid local format in the first place. We should not even get an error.

The basic problem is that jQuery validation uses regular JavaScript methods to do validation, thus some valid data, if in localized format, will produce validation errors. But even worse is the fact that some data validates just fine – but with a wrong value. E.g. point and comma in numeric values switch their meaning from en-US to de-DE, similarly day and month change their position between en-US and fr-FR.

The generally cited answer to solve this issue is to use Globalize from github… which requires a little handiwork.

 

First…

… you need to grab the globalize.js file from github under lib\ as well as the necessary localizations under lib\cultures\. If you require compressed versions (globalize.min.js below), you’ll have to create them yourself, e.g using http://jscompress.com/, as they are not provided. All files are then placed in the /Scripts folder of your web project.

Note: there is also a nuget package, which will install these files – including all localizations, but still no minified version.

 

Second…

… the scripts need to be included in our views. There are two options, which can both be found:

A) Define a respective bundle…

bundles.Add(new ScriptBundle("~/bundles/globalization")
    .Include("~/Scripts/globalize.js")
    .Include("~/Scripts/globalize.culture.*")
    .Include("~/Scripts/globalize.initialize.js")
);

and render it in _layout.cshtml – before the „script“ section is rendered, because that is where validation will come into play:

@Scripts.Render("~/bundles/globalization")
@RenderSection("scripts", required: false)

B) Instead of including all localization files (i.e. the globalize.culture.*.js files) you could also just include the necessary one. In that case it’s probably simpler to go without bundle:

@Scripts.Render("~/Scripts/globalize.js")
@Scripts.Render("~/Scripts/globalize.culture." + Culture + ".js")
@Scripts.Render("~/Scripts/globalize.initialize.js")

 

Third…

… we need to initialize the culture on the client side.

For this a call to Globalize.culture is necessary. You could generate the respective call an the server, passing in the current UI culture – which is what you’ll see very often in tutorials. However the information is already available on the client (because we put it there), thus a static script globalize.initialize.js will do (and will benefit http caching):

$(document).ready(function () {
    // use the language provided from server…
    var lang = $("html").attr("lang"); 
    if (typeof Globalize != ‘undefined’)
        Globalize.culture(lang);
});

 

Fourth…

…we need some plumbing.

We have the necessary infrastructure for localization, but jQuery validation does not use it yet. The final step is to wire jQuere validation with the new globalize methods. This is something that everyone has to do. Something that is quite simple. Something that could have been provided. Something that someone could have told you how to do. Guess what? Nobody did. [UPDATE: Turns out, I was wrong, see below. I still kept the following content for the explanations.]

jQuery has an object that provides the necessary validation methods: $.validator.methods. All that is necessary is to reroute them to replacements that make use of the globalization library. For MVC and unobtrusive validation, that requires just 5 methods: number, date, min, max, and range. In yet another additional script file globalize.validation.js:

// replace methods in jquery.validate.js ($.validator.methods) as necessary for localized validation:

$.validator.methods.number = function (value, element) {
    return this.optional(element) || !isNaN(Globalize.parseFloat(value));
}

$.validator.methods.date = function (value, element) {
    if (this.optional(element))
        return true;
    var result = Globalize.parseDate(value);
    return !isNaN(result) && (result != null);
}

$.validator.methods.min = function( value, element, param ) {
    return this.optional(element) || Globalize.parseFloat(value) >= param;
}

$.validator.methods.max = function( value, element, param ) {
    return this.optional(element) || Globalize.parseFloat(value) <= param;
}

$.validator.methods.range = function (value, element, param) {
    if (this.optional(element))
        return true;
    var result = Globalize.parseFloat(value);
    return (result >= param[0] && result <= param[1]);
}

Note: If you are using jQuery validation directly (not via unobtrusive validation), you’ll need to wire all methods in $.validator.methods respectively.

What‘s left is including this script in a bundle. Since there is already a bundle associated with validation and included in respective edit views, I just extended it:

var bundle = bundles.GetBundleFor("~/bundles/jqueryval");
bundle.Include("~/Scripts/globalize.validation.js");

 

And now our validation code works just fine, properly recognizing German date and number formats.

UPDATE: Just when I finished writing this post, I stumbled upon John‘s post, which is the first one I found that connects Globalize and jQuery validation both correctly and completely, and even provides a nuget package.

 

That’s all for now folks,
AJ.NET

January 19, 2014

ASP.NET MVC I18n – Part 7: Model Attributes

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 5:19 pm

The last post introduced resources and used them directly. When it comes to localizing views aspects based on data – i.e. databinding against models – resources are also the means to localize the data aspects.

Note: This post is part of a series

Localizing the displayed data is something that is fairly broadly documented, therefore I can keep the basics to the minimum and focus on the stuff that is rarely mentioned. If you need more background, the ASP.NET site provides the perfect starting point.

This post will concentrate on localizing the data related labels and validation messages; localizing the actual validation is going to be addressed in the next post.

 

The M-part in MVC is the model, in ASP.NET MVC usually consisting of POCOs. Data validation as well as localizing labels in MVC is based on attributes put on the data model. The very first question – and usually one that is not addressed – is one more related to architecture than to plain code: Which class shall I attribute?

Remember the contract data model that I introduced in the initial post:

namespace MyStocks.BusinessContract
{
    [DebuggerDisplay("BusinessContract.Stock: {ID} {Isin} {Name} {Price} {Date}")]
    public class Stock
    {
        public int ID { get; set; }
        /// <summary>
        /// http://en.wikipedia.org/wiki/International_Securities_Identification_Number
        /// 12-character alpha-numerical code
        /// </summary>
        public string Isin { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public DateTime Date { get; set; }
    }
}

Technically, we could just put attributes on this contract data model. However, that would result in UI-specific information bleeding into the business contract. (I elaborated on this topic in an earlier post.) So, to make it short, do NOT put view related attributes on those classes. Instead provide model classes intended for the presentation layer. For our little example the contract class is just copied to the model folder of our MVC project, but in my experience models in ASP.NET MVC have a tendency to become view related models (to avoid the term viewmodel) with the actual data just being one aspect. Now we can apply the necessary validation attributes:

[DebuggerDisplay("Models.Stock: {ID} {Isin} {Name} {Price} {Date}")]
public class Stock
{
    public int ID { get; set; }
    
    [Required]
    [RegularExpression(@"[a-zA-Z]{2}[\w]{9}[\d]{1}")]
    public string Isin { get; set; }
    
    [Required]
    public string Name { get; set; }
    
    [Required]
    [DisplayFormat(DataFormatString = "{0:f2}")] // currency would include currency sign
    public decimal Price { get; set; }
    
    [Required]
    [DataType(DataType.Date)]
    public DateTime Date { get; set; }
}

This of course requires mapping the data back and forth. For more complex applications you might consider something like AutoMapper for this task, but in simple cases plain code will do:

using ContractStock = MyStocks.BusinessContract.Stock; // business contract model
using ModelStock = MyStocks.Mvc.Models.Stock; // mvc model

namespace MyStocks.Mvc.Models
{
    static class Mapper
    {
        public static ModelStock MapToUI(this ContractStock stock)
        {
            return new ModelStock
            {
                Date = stock.Date,
                ID = stock.ID,
                Isin = stock.Isin,
                Name = stock.Name,
                Price = stock.Price
            };
        }
        
        public static ContractStock MapToContract(this ModelStock stock)
        {
            return new ContractStock
            {
                Date = stock.Date,
                ID = stock.ID,
                Isin = stock.Isin,
                Name = stock.Name,
                Price = stock.Price
            };
        }
    }
}

And call these methods as appropriate in the controller:

public class StockController : Controller
{
    protected MyStocks.BusinessContract.IStockService StockService { get; set; }
    
    public StockController()
    {
        this.StockService = new MyStocks.BusinessService.StockService();
    }
    
    //
    // GET: /Stock/
    public ActionResult Index()
    {
        var model = StockService.GetAllStocks().Select(Mapper.MapToUI);
        return View(model);
    }

(Of course, in a real-word application this is the place to employ DI, e.g. Unity.)

The (mostly generated) view picks the information up using extension methods:

@model IEnumerable<MyStocks.Mvc.Models.Stock>
@{
   
 ViewBag.Title = Labels.Navigation_Stock_Index;
}
<h2>@ViewBag.Title</h2>
<table>
    <tr>
        <th>@Html.DisplayNameFor(model => model.Isin)</th>
        <th>@Html.DisplayNameFor(model => model.Name)</th>
        <th>@Html.DisplayNameFor(model => model.Price)</th>
        <th>@Html.DisplayNameFor(model => model.Date)</th>
    </tr>
    @foreach (var item in Model)
    {
        <tr>
            <td>@Html.ActionLink(item.Isin, "Details", new { id = item.ID })</td>
            <td>@Html.DisplayFor(modelItem => item.Name)</td>
            <td style="text-align:right">@Html.DisplayFor(modelItem => item.Price) </td>
            <td>@Html.DisplayFor(modelItem => item.Date)</td>
        </tr>
    }
</table>
<br />
<br />
@Html.ActionLink(Labels.Navigation_Back, "Index", "Home")

If you run the application, the usual outcome is something like this:

As you can see, property names are used for field labels. Jumping to details and edit view, we will also get validation messages based on our attributes from above:

You might already see the respective validation messages properly translated. This depends on the language version of the installed .NET Framework. Generally I would be careful to rely on the fact that all necessary languages are installed in production. Latest when custom feedback is required you need to provide the message in any case.

Providing localized validation messages, as well as display names for labels, can be done providing the „link“ to the respective resources, using attributes in the MVC model. DisplayAttribute for the display name, and the validation attributes for respective error messages:

[DebuggerDisplay("Models.Stock: {ID} {Isin} {Name} {Price} {Date}")]
public class Stock
{
    public int ID { get; set; }
    
    [Required(ErrorMessageResourceType = typeof(Localizations.Models), ErrorMessageResourceName = "Validation_Required")]
    [RegularExpression(@"[a-zA-Z]{2}[\w]{9}[\d]{1}", ErrorMessageResourceType = typeof(Localizations.Models), ErrorMessageResourceName = "Stock_Isin_RegEx")]
    [Display(ResourceType = typeof(Localizations.Models), Name = "Stock_Isin")]
    public string Isin { get; set; }
    
    [Required(ErrorMessageResourceType = typeof(Localizations.Models), ErrorMessageResourceName = "Validation_Required")]
    [Display(ResourceType = typeof(Localizations.Models), Name = "Stock_Name")]
    public string Name { get; set; }
    
    [Required(ErrorMessageResourceType = typeof(Localizations.Models), ErrorMessageResourceName = "Validation_Required")]
    [Display(ResourceType = typeof(Localizations.Models), Name = "Stock_Price")]
    [DisplayFormat(DataFormatString = "{0:f2}")] // currency would include currency sign
    public decimal Price { get; set; }
    
    [Required(ErrorMessageResourceType = typeof(Localizations.Models), ErrorMessageResourceName = "Validation_Required")]
    [Display(ResourceType = typeof(Localizations.Models), Name = "Stock_Date")]
    [DataType(DataType.Date)]
    public DateTime Date { get; set; }
}

That much can be found in any tutorial. What is also rarely mentioned though (much less documented), is that these localized messages can use string.Format placeholders. Let’s state that clearly: You do not have to provide „field XY is required“ messages for each and every field! (as some posts imply…).

All validation attributes support at least a placeholder for the field name, most also their additional parameters, such as the length in MinLengthAttribute. In other words, those resources would be sufficient for all respective fields:

Validation_Required= „{0} is a required field!“
Validation_MaxLength= „{0} must be shorter or equal to {1} characters!“

BTW: In case you do not like to clutter your source with all these addition parameters in your attributes, Phil uses the MVC infrastructure to resolve and inject localized messages using a convention based approach.

 

Once this is done, we get localized validation messages – with one final hole to patch: Look at all those nicely localized labels and messages… and the one culprit refusing to cooperate, the error message on the date (“Datum”) field:

The reason this does not work is that this message does not come from validation, but from binding. Binding is what turns the text into the appropriate data type (which implies „syntactic correctness“), while semantic validation checks the resulting value against some rules. See here for background.

The immediate consequence is that localization is done differently. Actually there are only two possible cases, numbers and dates, and there is only one way to supply them:

  • Define a resource file in App_GlobalResources – yes, I said, don‘t use it. This is the exception that proves the rule!
  • Define two messages with exactly these names:

  • Tell the model binder to use that particular resource file:

public static void InitializeLocalization()
{
    ClientDataTypeModelValidatorProvider.ResourceClassKey = "GlobalResources";
}

You might have seen DefaultModelBinder.ResourceClassKey for a similar purpose. Apparently this property is not used any more…

And now it works:

Wonder why it works in that twisted way?

“Aside from the standard localization of MVC, this message isn’t easily changeable (it was added at the last minute, and we didn’t offer an override unfortunately).” see here, one of Brad Wils‘ comments.

LOL!

 

Just for completeness: All this is based on the fact that MVC uses jQuery unobtrusive validation. Should you be using jQuery validation directly, you have to adjust $.validator.messages. See here for a complete example.

That’s all for now folks,
AJ.NET

January 13, 2014

ASP.NET MVC I18n – Part 6: Using .NET Resources

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 6:47 pm

The last part addressed larger chunks of HTML content. In that case, imperative coding works nicely. For labels, menu links and similar texts however, the standard localization mechanism in .NET is resources, based on manifest resources, and respective satellite assemblies.

Note: This post is part of a series

Access is handled via the ResourceManager class, additionally Visual Studio will generate an accessor class for each .resx file, that offers members for each key. This eliminates the possibility of typos and provides more convenience through intellisense. For MVC applications it is necessary to change the access modifier for that class to public, as shown here:

The reason is that ASP.NET works with code generation for views (WebForms as well as Razor), thus the generated code lives in another temporary assembly, not the one associated with the web project, and won’t have access to an internal class.

Please note that this applies to the generated accessor class, not the resource itself. One can always use ResourceManager and access a resource in any other assembly.

And to get another point out of the way: With ASP.NET WebForms you might have used the special folders App_GlobalResources and App_LocalResources to place your .resx files in. Don‘t do that with ASP.NET MVC!

The problem is that resources files in these folders are treated differently by Visual Studio and the ASP.NET runtime. There are workarounds to make it work, but why rely on quirks if another approach works without.

If you are interested in the background, Scott has further information.

 

I chose to place my resources in my own special folder. Since the obvious name for that folder (“Resources”) is already taken (by the default resource file generated from the project settings as well as as namespace by App_GlobalRessources), I named it Localizations:

The resource file Labels.resx and its localized version Label.de-DE.resx contains all navigation labels used in menus and other links.

Adding this folder to the web.configs (the one in the Views folder)…

…makes the view code even more convenient:

<ol class="round">
    <li class="one">
        @Html.ActionLink(Labels.Navigation_Stock_Index + "…", "Index", "Stock")
    </li>

Proper localized information in respective .resx files available, and we have the next localization step solved:

 

Final hint: I can live with that approach, i.e.maintain resources in arbitrary files, that I organize as I like. Should you want to “attach” resources to you views and have them used automatically, you might want to have a look at Matt’s post. It should also be possible to implement a simple fallback scheme that allows keeping general labels automatically in one centralized resource file, while maintaining specific labels in view specific resource files.

That’s all for now folks,
AJ.NET

December 14, 2013

ASP.NET MVC I18n – Part 5: Imperative View Localization

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 10:58 am

This is going to be a simple one…

Note: This post is part of a series

The most simple way to accommodate different languages is by direct, imperative coding. E.g. images can be localized by maintaining the region as part of the filename and appending it via code. So, let’s make our language switcher from earlier a little nicer:

<a href="@url"><img src="~/Content/flag_@(currentCulture.Name).gif" height="12" width="20" />
     <img src="~/Content/arrow.png" height="12" width="9" />
     <img src="~/Content/flag_@(nextCulture.Name).gif" height="12" width="20" /></a>

Resulting in respective flag images in the upper right corner:

 

As another example, that feedback message regarding the UI‘s region could be localized the following way:

@switch (UICulture)
{
    case "de-DE":
        <p>Derzeitige Sprache ist: @UICulture</p>
        break;
    default:
        <p>Current culture is: @UICulture</p>
        break;
}

 

For smallish content that certainly works, but once the affected fragment spans considerable amount or even most of the view, it‘s probably better to provide separate partial views. For example for the feature text (the blue area) on the index view:

@{ Html.RenderPartial("_Index_featured_" + UICulture); }

… which uses the _Index_featured_en-US.cshtml and _Index_featured_de-DE.cshtml partial views respectively. Careful, though. There is no fallback in this code, meaning you have to provide all localized versions, which may not always be necessary or feasible. However, it‘s quite easy to come up with a helper method, that looks for the respective files and implements proper fallback strategies.

 

This imperative approach works well for the occasional demand. However, if this becomes a regular demand, you will start looking for better solutions. E.g. you might want to place the localized views beside the regular ones with the region as part of the filename (or in region specific folders with the same filename). Just be careful that you preferred strategy does not collide with other features, such as mobile views or areas. (There are already too many competing demands.)

I do not need to walk you through respective solutions however, as Vlad has already written about the topic. He adds region specific view folders and evaluates them using his own view engine (for WebForms, but the principle holds for Razor as well). You could of course apply a different naming scheme, e.g. attach the region to the view name itself.

That’s all for now folks,
AJ.NET

December 8, 2013

ASP.NET MVC I18n – Part 4: CSS Styles

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 11:18 am

We have our user‘s language choice acknowledged and can start making use of it.

Note: This post is part of a series

Let‘s start with a topic that is not exactly MVC (which is probably why so many tutorials don‘t mention it), but I would like to get it out of our way: region specific information in CSS style sheets.

Of course it is a characteristic of ASP.NET MVC, compared with classical WebForms, that one needs a far better understanding of the underlying technologies, such as HTTP, HTML, CSS. So, in a way, talking about CSS is very much in line with ASP.NET MVC ideas…

For my little sample application I provided the logo image as background image of the header:

<body>
    <header>
       
<div class="content-wrapper head">
           
<div class="float-right">
               
<section id="login">
                    @{ Html.RenderPartial("_SetPreferredCulture"); }
                </section>
            </div>
        </div>
    </header>

The background comes with the head style:

.head {
    background-image: url(‘logo_en-US.png’);
    background-repeat: no-repeat;
    height: 100px;
}

That much s independent of the region.

For the region to come into play, we need it present in our html content, which is usually done on the root element in _layout.cshtml:

<!DOCTYPE html>
     <html lang="@UICulture" xml:lang="@UICulture">

UICulture is readily available from the view class. Putting the language in the lang and the xml:lang attribute (provided you maintain XML conformity) – and omitting it anywhere else, like in the http-equiv header Content-Language – is the recommended way of providing this information.

Also here: “The Content-Language value for an http-equiv attribute on a meta element should no longer be used. You should use a language attribute on the html tag to declare the default language of the actual text in the page.”

http://www.w3.org/International/questions/qa-http-and-lang

Now we can use the lang pseudo-class in CSS to provide two different backgrounds:

.head {
    background-image: url(‘logo_en-US.png’);
    background-repeat: no-repeat;
    height: 100px;
}
:lang(de-DE) .head {
    background-image: url(‘logo_de-DE.png’);
}

And it works, too Zwinkerndes Smiley

That‘s it. Simple and easy.

 

One (rare) pitfall…

Suppose the current culture is „de-DE“ (and and advertised on the lang attribute on the HTML element), and the application has some „information area“ for arbitrary content. If that particular content is only available in English, you might want to set the lang attribute of the respective DIV to „en-US“, perhaps to show some flag as background.

Now, common expectation and standards would agree that the DIV and it’s content would now be English instead of German:

“An element inherits language code information according to the following order of precedence (highest to lowest):

  • The lang attribute set for the element itself.
  • The closest parent element that has the lang attribute set (i.e., the lang attribute is inherited).

[...]”

http://www.w3.org/TR/REC-html40/struct/dirlang.html#h-8.1.2

And in HTML5:

“To determine the language of a node, user agents must look at the nearest ancestor element (including the element itself if the node is an element) that has a lang attribute in the XML namespace set or is an HTML element and has a lang in no namespace attribute set. That attribute specifies the language of the node (regardless of its value).”

http://www.w3.org/TR/html5/dom.html#the-lang-and-xml:lang-attributes

In other words; The language is determined by looking for the lang attribute in the parent elements until found. This would imply that the language of a node and its subnodes changes with a lang attribute.

What actually happens, however – in IE and Firefox anyway – is that the language does not change, but is added. Both language tags, the one on the DIV and the one on the HTML element are “active”. And also CSS styles for both languages that use the pseudo-class. So the little image you wanted to indicate the language is determined by the precedence of the language dependent styles – not the language itself.

Granted, it’s rarely an issue. But if it is it can be very annoying!

 

That’s all for now folks,
AJ.NET

November 30, 2013

ASP.NET MVC I18n – Part 3: Custom Language Choice

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 5:49 pm

The last post showed how we could honor the language preferences the user declared in his browser. However, it is also good practice to let the user change the region, independent of the browser settings.

Note: This post is part of a series

To achieve this, we need:

  • a way to maintain the user’s culture choice overriding the browser preferences
  • a way to actually change the preferred culture

 

Maintaining the preferred culture… 

As we know, each request comes with the accept-language header, telling us language preferences set in the browser. We need something similar to maintain the overriding culture choice.

One option, that is used very often used, is including the region in the URL. This implies addressing the culture in every route declaration, and every action link needs to provide the culture as additional parameter.  Alex shows how the URLs can be analyzed and the culture applied using a custom MvcRouteHandler; Nadeem does it in a Controller base class with similar intentions. Something in the same line should be possible for generating URLs (e.g. with a custom route class).

This certainly works and there is nothing wrong with it. Still, I don’t like this approach all that much. From a technical perspective, URLs in static content like CSS files are still not addressed properly. Additionally there are also other MVC features affecting URLs as well, e.g. areas, which might cause problems. And from a more “semantic” perspective, URLs are meant to address "resources" – which is a hard enough task already. Language preferences, similarly to skin and other profile data, is kind of "orthogonal" information affecting the rendering, but not addressing the resource. Unfortunately MVC does not really give an answer on how to pass that kind of information, which is why you can find all possible approaches (URL parts, query params, cookies, user session, …), none of which is always good or always bad.

For the problem at hand, the preferred culture, my choice is using a cookie: Adjusting the URL feels too invasive, query params are too error prone, and no need to enforce a session.

 

Setting the culture in a cookie is quite simple:

const string CookieName = "PreferredCulture";

public static void SetPreferredCulture(this HttpResponseBase response, string cultureName)
{
    SetPreferredCulture(response.Cookies, cultureName);
}

static void SetPreferredCulture(HttpCookieCollection cookies, string cultureName)
{
    var cookie = new HttpCookie(CookieName, cultureName);
    cookie.Expires = DateTime.Now.AddDays(30);
    cookies.Set(cookie);
    Debug.WriteLine("SetPreferredCulture: " + cultureName);
}

Reading likewise:

static CultureInfo GetPreferredCulture(HttpCookieCollection cookies)
{
    var cookie = cookies[CookieName];
    if (cookie == null)
        return null;
    var culture = GetCultureInfo((string)cookie.Value);
    if (culture == null)
        return null;
    if (!SupportedCultures.Where(ci => ci.Name == culture.Name).Any())
        return null;
    return culture;
}

I don’t trust the cookie to contain a valid value. Call me paranoid, but somebody could have tampered with the request.

And we need to change our method to determine the current culture based on the cookie value:

public static void ApplyUserCulture(this HttpRequest request)
{
    ApplyUserCulture(request.Headers, request.Cookies);
}

static void ApplyUserCulture(NameValueCollection headers, HttpCookieCollection cookies)
{
    var culture = GetPreferredCulture(cookies)
        ?? GetUserCulture(headers)
        ?? SupportedCultures[0];
    
    var t = Thread.CurrentThread;
    t.CurrentCulture = culture;
    t.CurrentUICulture = culture;
    Debug.WriteLine("Culture: " + culture.Name);
}

 

Changing the preferred culture…

Now that we have the groundwork in place, we need to surface the feature to the user. Starting MVC-like with a controller and action to set the preferred culture:

public class CultureController : Controller
{
    //
    // GET: /SetPreferredCulture/de-DE
    public ActionResult SetPreferredCulture(string culture, string returnUrl)
    {
        this.Response.SetPreferredCulture(culture);
        if (string.IsNullOrEmpty(returnUrl))
            return RedirectToAction("Index", "Home");
        return Redirect(returnUrl);
    }
}

And the necessary route…

var route = routes.MapRoute(
    name: "SetPreferredCulture",
    url: "SetPreferredCulture/{culture}",
    defaults: new { controller = "Culture", action = "SetPreferredCulture", culture = UrlParameter.Optional }
);

For the UI it’s really up to you how you present the choice: some drop down list is quite common. I intend to provide a single action link that simply choses the next supported culture, thus repeatedly clicking it would cycle through all cultures. For this a little helper method makes life simpler:

public static void GetSwitchCultures(out CultureInfo currentCulture, out CultureInfo nextCulture)
{
    currentCulture = Thread.CurrentThread.CurrentUICulture;
    var currentIndex = Array.IndexOf(SupportedCultures.Select(ci => ci.Name).ToArray(), currentCulture.Name);
    int nextIndex = (currentIndex + 1) % SupportedCultures.Length;
    nextCulture = SupportedCultures[nextIndex];
}

Based on the work so far I can implement a _SetPreferredCulture.cshtml partial view:

@{
    // cycle through supported cultures
    System.Globalization.CultureInfo currentCulture;
    System.Globalization.CultureInfo nextCulture; 
    MyStocks.Mvc.Helper.CultureHelper.GetSwitchCultures(out currentCulture, out nextCulture);
    string currentCultureDisplayName = currentCulture.Parent.NativeName;
    string nextCultureDisplayName = nextCulture.Parent.NativeName;
    string linkText = currentCultureDisplayName + " => "+ nextCultureDisplayName; 
    string url= Url.Action("SetPreferredCulture", "Culture", new { culture = nextCulture.Name, returnUrl = Request.RawUrl });
}
<div>
    @Html.ActionLink(linkText, "SetPreferredCulture", "Culture", new { culture = nextCulture.Name, returnUrl = Request.RawUrl }, null)
</div>

Include it in the _Layout.cshtml view and my users can change their culture as they wish:

Done.

 

That’s all for now folks,
AJ.NET

November 23, 2013

ASP.NET MVC I18n – Part 2: Detect Browser Settings

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 5:47 pm

If you want to cater to the user’s language and region choice, the first thing to do is actually take note of his choice.

Note: This post is part of a series

While you could simply start with one default language and let him switch the language using some control, it is probably far more "polite" to use the fact that he already tells you his preferences: Each browser has the ability to let the user chose his is language preferences:

This information is sent with each request via the accept-language http header, which contains a collection of weighted language tags, say the following header with the above settings:

Accept-Language: de-DE,de;q=0.8,en-US;q=0.5,en;q=0.3

This is very basic stuff and I’m only going into this detail because I have actually seen code that uses the header value as if it contains exactly one language and ignoring the weight.

The simple approach…

“All” we have to do is grab the language from the header and tell .NET for each request:

protected void Application_OnBeginRequest()
{
    CultureHelper.ApplyUserCulture(this.Request);
}

The CultureHelper class contains the plumbing, we’re going to set up:

public static void ApplyUserCulture(this HttpRequest request)
{
    ApplyUserCulture(request.Headers);
}

static void ApplyUserCulture(NameValueCollection headers)
{
    var culture = GetUserCulture(headers) ?? CultureInfo.GetCultureInfo("en-US");
    
    var t = Thread.CurrentThread;
    t.CurrentCulture = culture;
    t.CurrentUICulture = culture;
    Debug.WriteLine("Culture: " + culture.Name);
}

Actually determining the user’s choice is simple: 

static CultureInfo GetUserCulture(NameValueCollection headers)
{
    // Accept-Language: de, en-US;q=0.8
    string acceptLanguages = headers["Accept-Language"];
    if (string.IsNullOrEmpty(acceptLanguages))
        return null;
    // split languages _and_ weights
    var cultureName = acceptLanguages.Split(‘,’, ‘;’)[0];
    return GetCultureInfo(cultureName);
}

private static CultureInfo GetCultureInfo(string language)
{
    try
    {
        return CultureInfo.GetCultureInfo(language);
    }
    catch (CultureNotFoundException)
    {
        return null;
    }
}

Note the exception handling. This is necessary due to the different notions of "language" (see the last post), and the possibility that .NET might not support Klingon. Most people will never actually encounter this issue during regular operations, but there is the occasional exception (no pun intended). And of course the possibility of malicious requests…

Accessing the header value and setting the cultures is something that can be found in every blog post and tutorial. Actually ASP.NET would do this automatically (see also Scott’s post).

Unfortunately this is simply too shortsighted.

 

Matching the user’s language choice…

I don’t want the user’s first language choice applied unconditionally. I want the user’s language choices that I can best support with my supported cultures.

My application is going to support English and German. What if my French colleague showed up? We could set the culture to “fr-FR” (as, in fact, the above code will do). Since there are no French localizations, proper fallback strategy should ensure he gets English texts. Is this really the best choice, if he asks for “fr-FR,de-DE,en-US”? And what about date formats? Is the meaning of 01/06/2013 immediately obvious? Does he even suspect, that – within an otherwise English UI – this is actually the first of June (interpretation as dd/mm/yyyy) – not epiphany (mm/dd/yyyy)?
Thus it is better to present him a consistent (in this case American English, i.e. en-US) version, rather than a confusing mix.

So, what I want is matching the users’ list of preferred regions (all of the header values, not just the first one!) to the collection of regions my application supports, and set the culture to the best match.

This is something I have rarely seen addressed, much less correctly. E.g. Nadeem is one of the few who actually recognized that necessity; still, his implementation does not quite meet my expectations.

 

Let’s make that crystal clear: Say my application supports en-US and de-DE, with en-US the default.

Perfect matches:

  • If the user’s request is "de-DE, en-US;q=0.8", then de-DE is the perfect match. (Obviously.) Likewise, if the user’s request is "fr-FR, en-US;q=0.8", then en-US is the perfect match. Not the users’ first choice, but anyway. Also if the user’s request is "fr-FR, de-DE;q=0.8, en-US;q=0.5", then de-DE is the perfect match. This is a case which most, if not all implementations I have seen, are missing.
  • Something that is also missed quite often: If his request is "de, en-US;q=0.8", then de-DE is still the correct choice, because de encompasses all German regions.
  • If his request is "de-AT, en-US;q=0.8", then en-US is the best choice, because it’s a perfect match, while de-AT matches de-DE only partly. This is debatable, but since he could control that via the browser settings, it’s better than other interpretations.

Partial matches:

  • If his request is "de-AT, fr-FR;q=0.8", then we have no perfect match. But de-DE matches de-AT at least partly, so it would be a better choice, than using the fallback to the default region en-US.

No match:

  • Only if no requested region matches any of the supported cultures even slightly, e.g. "es-ES, fr-FR;q=0.8", the default region en-US is used – nothing else that could be done.

Now, this requires a little more code than before…

 

Parsing the header…

The Accept-Language header generally contains entries with weights (usually they are sorted according to weight anyway, but I don’t like to rely on that). Parsing it is relatively straightforward, but requires some groundwork:

public static CultureInfo[] GetUserCultures(string acceptLanguage)
{
    // Accept-Language: fr-FR , en;q=0.8 , en-us;q=0.5 , de;q=0.3
    if (string.IsNullOrWhiteSpace(acceptLanguage))
        return new CultureInfo[] { };
    
    var cultures = acceptLanguage
        .Split(‘,’)
        .Select(s => WeightedLanguage.Parse(s))
        .OrderByDescending(w => w.Weight)
         .Select(w => GetCultureInfo(w.Language))
         .Where(ci => ci != null)
         .ToArray();
    return cultures;
}

WeightedLanguage is a little helper class:

class WeightedLanguage
{
    public string Language { get; set; }
    public double Weight { get; set; }
    
    public static WeightedLanguage Parse(string weightedLanguageString)
    {
        // de
        // en;q=0.8
        var parts = weightedLanguageString.Split(‘;’);
        var result = new WeightedLanguage { Language = parts[0].Trim(), Weight = 1.0 };
        if (parts.Length > 1)
        {
            parts[1] = parts[1].Replace("q=", "").Trim();
            double d;
            if (double.TryParse(parts[1], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out d))
                result.Weight = d;
        }
        return result;
    }
}

Maintaining supported cultures…

Getting the supported cultures can be done via some configuration… – or just be hardcoded. So let’s just assume we have the following array, and we will use first culture as default culture:

public static readonly CultureInfo[] SupportedCultures = new CultureInfo[]
{
    CultureInfo.GetCultureInfo("en-US"),
    CultureInfo.GetCultureInfo("de-DE"),
    //CultureInfo.GetCultureInfo("fr-FR"),
    //CultureInfo.GetCultureInfo("es-ES"),
};

Matching…

Now we can start matching the user’s cultures and the supported cultures.

public static CultureInfo GetUserCulture(NameValueCollection headers)
{
    var acceptedCultures = GetUserCultures(headers["Accept-Language"]);
    var culture = GetMatchingCulture(acceptedCultures, SupportedCultures);
    return culture;
}

Actually matching requires two passes. The first pass is looking for the perfect match; if none is found, the second pass looks for an imperfect match:

public static CultureInfo GetMatchingCulture(CultureInfo[] acceptedCultures, CultureInfo[] supportedCultures)
{
    return
        // first pass: exact matches as well as requested neutral matching supported region
        // supported: en-US, de-DE
        // requested: de, en-US;q=0.8
        // => de-DE! (de has precendence over en-US)
        GetMatch(acceptedCultures, supportedCultures, MatchesCompletely)
        // second pass: look for requested neutral matching supported _neutral_ region
        // supported: en-US, de-DE
        // requested: de-AT, en-GB;q=0.8
        // => de-DE! (no exact match, but de-AT has better fit than en-GB)
        ?? GetMatch(acceptedCultures, supportedCultures, MatchesPartly);
}

GetMatch traverses the user’s accepted cultures, and finds the first matching supported culture:

public static CultureInfo GetMatch(
    CultureInfo[] acceptedCultures, 
    CultureInfo[] supportedCultures,
    Func<CultureInfo, CultureInfo, bool> predicate)
{
    foreach (var acceptedCulture in acceptedCultures)
    {
        var match = supportedCultures
            .Where(supportedCulture => predicate(acceptedCulture, supportedCulture))
            .FirstOrDefault();
        if (match != null)
            return match;
    }
    return null;
}

The predicates do the actual match for a pair of cultures. For the perfect match this includes a requested neutral culture matching a region, see the second example above.

static bool MatchesCompletely(CultureInfo acceptedCulture, CultureInfo supportedCulture)
{
    if (supportedCulture.Name == acceptedCulture.Name)
        return true;
    // acceptedCulture could be neutral and supportedCulture specific, but this is still a match (de matches de-DE, de-AT, …)
    if (acceptedCulture.IsNeutralCulture)
    {
        if (supportedCulture.Parent.Name == acceptedCulture.Name)
            return true;
    }
    return false;
}

For an imperfect match, we simply compare the neutral cultures (i.e. the parents of specific cultures):

static bool MatchesPartly(CultureInfo acceptedCulture, CultureInfo supportedCulture)
{
    supportedCulture = supportedCulture.Parent;
    if (!acceptedCulture.IsNeutralCulture)
        acceptedCulture = acceptedCulture.Parent;
    
    if (supportedCulture.Name == acceptedCulture.Name)
        return true;
    return false;
}

Finally.

You may want to print the culture on the home view:

Current culture is: @UICulture

UICulture is a shortcut available from the view base class.

Quite a bunch of code one has to write himself for a regular demand, if you ask me.

That’s all for now folks,
AJ.NET

November 15, 2013

ASP.NET MVC I18n – Part 1: Basics

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 9:15 pm

When I started the series, I didn’t even plan this particular post. But as I drafted the upcoming posts, I realized, we cannot avoid taking a look at what we are actually dealing with. One should know, what he is talking about. Thus, here’s the necessary theoretical background…

The one thing we need to be aware is that we are dealing with two different contexts: The server part, defined by the .NET Framework, and the client side, defined by HTML et al. These contexts differ in the terms they use, in their customs, and in their technical scope.

Server side…

.NET maintains the necessary information via the CultureInfo class:

“The CultureInfo class specifies a unique name for each culture, based on RFC 4646. The name is a combination of an ISO 639 two-letter lowercase culture code associated with a language and an ISO 3166 two-letter uppercase subculture code associated with a country or region.”

In short, we are talking about "en-US", "de-DE", and so on (ignoring special cases). One distinction is made regarding neutral cultures (associated only with the first part, e.g. "en" and "de"), and specific cultures, associated with the country or region. Still, neutral cultures are still maintained in CultureInfo instances, including information beyond the language. They generally rely on the "major representative" of that language. i.e. Germany for German (sorry Austrians ;-)), and the United Kingdom of Great Britain and Northern Irland  … no wait… that former colony of theirs Zwinkerndes Smiley, for English.

It should be noted, that CultureInfo deals with all aspects regarding regions: It acts as language selector, provides date and time formats, even the calendar is addressed.

Regarding localized content (like strings for labels), .NET uses a system of resources and satellite assemblies, that are accessed via the ResourceManager class, either directly or through generated code. (I will assume that this is basic .NET knowledge and not go into further details about it.)

All in all, a comprehensive and consistent system.

Client side…

HTML traditionally only addresses languages (not date or number formats):

“The lang attribute’s value is a language code that identifies a natural language spoken, written, or otherwise used for the communication of information among people.”

http://www.w3.org/TR/REC-html40/struct/dirlang.html#h-8.1.1 

HTML is also far more open in regard to how a language is identified. This includes, but goes beyond what .NET supports:

“Here are some sample language codes:

  • "en": English
  • "en-US": the U.S. version of English.
  • "en-cockney": the Cockney version of English.
  • "i-navajo": the Navajo language spoken by some Native Americans.
  • "x-klingon": The primary tag "x" indicates an experimental language tag”

http://www.w3.org/TR/REC-html40/struct/dirlang.html#h-8.1.1 

However, the focus of HTML is also limited to languages, to the point of actively ignoring any other localization demand:

“The golden rule when creating language tags is to keep the tag as short as possible. Avoid region, script or other subtags except where they add useful distinguishing information. For instance, use ja for Japanese and not ja-JP, unless there is a particular reason that you need to say that this is Japanese as spoken in Japan, rather than elsewhere.”

http://www.w3.org/International/articles/language-tags/

And, indeed, you’ll find that most localized HTML or CSS code you may come across (in samples and documentation) uses two-letter language codes.

Alas, with HTML5 and the new input controls, the focus on "language" is no longer sufficient. A date picker does not only change the weekday names, but also the date format and the first day of the week. The way this issue is addressed by the W3C however seems a bit helpless and places the issue on the browser vendors:

“Browsers are encouraged to use user interfaces that present dates, times, and numbers according to the conventions of either the locale implied by the input element’s language or the user’s preferred locale.”

http://www.w3.org/TR/html5/forms.html#input-impl-notes

Well, a little further down they are refreshingly honest:

“There’s still a risk that the user would end up arriving a month late, of course, but there’s only so much that can be done about such cultural differences…”

BTW: Whenever I wrote HTML, this also included XML, CSS, and HTTP (here and here).

Regarding localized content, HTML only allows denoting the language by the lang attribute. You can mix different languages in one document, but HTML itself does not do anything further. CSS selectors on the other hand can be used to attach styles depending on the language. 

 

Consequences?

For an LOB application, using "language" in the limited sense of HTML is far to shortsighted, thus I will use regions (respective specific cultures on the server, respective tags with language code and country or region), whenever possible. This may seem odd in HTML or CSS, but so what?

And the next post will contain some code, promise.

That’s all for now folks,
AJ.NET

Older Posts »

The Shocking Blue Green Theme Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

Join 244 other followers