Multilingual Websites in Umbraco 8
Heads Up!
This article is several years old now, and much has happened since then, so please keep that in mind while reading it.
My aim is to guide you through the different problems you may encounter and, hopefully, provide advice, some solutions and even the odd code snippet you can use.
Who’s It For?
I guess the target audience for this article are experienced Umbraco developers who have never built a multi language site in Umbraco 8 but are considering doing so.
It’s important to note that this isn’t a step-by-step guide to creating a multilingual site - it’s more of a high-level take on what you will encounter and what to consider before embarking on your own journey. It contains details of the issues I encountered - which you’ll likely also encounter - so they won’t surprise you if they should arise.
Before we start, though, it’s worth reviewing the options we had for building multilingual sites before Umbraco 8 came along.
Umbraco 7
In Umbraco 7 (and earlier) we had two main ways of building a multi-language site:
Multi Tenancy
This is where you would have multiple home pages within one website, each containing pages devoted to a separate language. In order to create a new language you’d clone the homepage and children, change the language / hostname and then an editor would be expected to go through and translate the copy. It was probably the most common way of creating multilingual websites in Umbraco, and worked quite well, but wasn’t necessarily very easy for editors to work with.
For instance, if you had 8 languages and an editor wanted to create a new page in each, they would have to go into each site, locate the relevant parent page and then create the new page (or copy it over) 8 times. Assets (such as media) would be copied rather than shared, meaning that to change an image across all languages would involve having to edit multiple pages. It wasn’t the “friendly” experience we would hope - but it did work and was relatively straightforward to produce.
One to One
This method required only one instance of a page - solving one of our issues with multi-tenancy - but added a slew of new problems to solve. How do we add different copy for each language to one document type? The answer would typically be either naively adding multiple fields (eg. Heading_en, Heading_dk etc) or using a package like Vorto which cleverly allowed you to “wrap” property editors, allowing you to have one field but many languages.
Whilst these methods worked, they never felt “native” to Umbraco and still left you with many problems to solve, such as creating unique URLs for each language. The fact is, Umbraco 7 and earlier were never designed to be used in this way.
Umbraco 8 To The Rescue
When Umbraco 8 was released in February 2019 (another lifetime to most of us now!) it heralded “language variants” as one of its Big Three Features. It promised:
“Wave goodbye to workarounds, hacks, and syncing with external packages. In Umbraco 8, management of language variants is part of the CMS. Based on popular demand, we have made it possible for you to update, create and manage your multilingual content in a simpler way.”
In other words, Umbraco 8 had been architected with the concept of multilingual content within its core. It was baked in, rather than needing to be hacked on. It was one of it’s biggest selling points in all the promotional literature.
However, it’s important to realise that the approach of Umbraco 8 was specifically aimed at creating One to One sites (you can, of course, still use the multi-tenancy approach but this is not what Umbraco 8 encourages or promotes). Bear that in mind.
New Umbraco 8 Features
Very quickly let's look at what is new in Umbraco 8 in regard to languages:
- Variants - this takes the concept of Vorto but bakes it into the core of Umbraco, so that any property can be varied by language.
- Inheritance - you can configure Umbraco so that it will "fall back" to a default language if no content is supplied. You can also make a language mandatory.
- Preview - you can easily switch between languages in the Umbraco preview.
- Switching - a new language menu sits over the content tree to make it simple to switch which language is currently being edited.
- Side by side editing - the ability for editors to see a split view between two languages, making it much easier to translate content.
Again, all these methods very much encourage you down the road of creating One to One sites. If you want to read more about these features then check out Harriet Lawrie's great overview article Multilingual websites in Umbraco 8 from 2019's '24 Days in Umbraco'.
Think Carefully!
My first (and most important) piece of advice would be to find out how your client will be managing their content and whether a One to One solution actually fits with their requirements. Do this before you do anything else. A thorough "discovery phase" is key to success as it ensures your assumptions and your client's assumptions actually align!
Whilst a One to One solution undoubtedly makes editing multilingual content much more friendly there are some things to consider first:
- Will each site have the same content and structure? Or is there a requirement to have different content and structures for each site? If the latter, then a One to One solution might not be the best fit.
- Will there be a mandatory language that is required before a page can be published? In this case Umbraco 8 makes this simple, so it’s a good choice.
- Is there a requirement so that editors can only edit a specific language? Currently there is no way to limit editor permissions to a specific language in Umbraco 8 - there is an open Github issue for this. If this is important to your client then you need to discuss this and be open.
- Do you want to share content between language variants? For instance, share the same mast hero image across all pages? In this case, Umbraco 8 is a good fit.
- Will your client want to create content in one language that doesn’t appear in the other sites? Whilst this is perfectly possible in Umbraco 8 it requires some thought on how to set-up, since the assumption of One to One is always that there is a “lead” or default language which the other content is translated from. If the sites are all going to contain essentially different content you may be better going the multi-tenancy way.
Setting Up Umbraco 8
I’m not going to go into great detail about how to set up a multilingual site in Umbraco as this is covered in the documentation. However, it’s worth reminding ourselves of these key things:
Languages
- You first create the languages you want in Settings. You will always have to create one default language and there can only be one default language.
- You can configure a language to be mandatory ie. all required fields must be populated for that language before you can publish any page.
- You can set a language so that it will “fall back” to any other language you have created. For instance, if you have a US and UK site you can make it so that the UK site will fall back to showing the US content if none is supplied for the UK site (we’ll just have to overlook the fact that Americans can’t spell :p).
Host Names and Domains
For each language you create you will need to bind a hostname and culture (language) to your root page (usually the home page). You can use either fully qualified domain names or just use a root path fragment (eg. /en/ or /es). Just remember to do this for every language you create, otherwise you will find you can publish pages but they won’t be rooted to a path and won’t be given a URL. (I made this mistake once and spent an hour wondering why my page didn’t have a URL for the Chinese version!).
Variants
It’s important to realise that variants work on two levels - on the entire Document Type and also at the individual Property level. You first need to enable “Allow varying by culture” on the doc type - this signals to Umbraco you want to enable language variants for the document. Once you have done that you can then enable a similar setting on each individual property you create. In other words, you can decide on a property by property basis whether you want to enable language variants for that content. Let’s consider this for a moment…
Let’s imagine we are creating an Events section with events for five different languages where the requirement is that every event has to be translated into each configured language. Now a very simple Event might have three different properties:
- Date - a date picker
- Image - a media picker
- Content - a rich text editor for the main copy
Let’s assume these are all mandatory (required) properties - they all need to be set to publish your event.
It would make sense to enable “Allow vary by culture” on the Content rich-text property - this is copy that needs translating. But the event date will be the same for all languages - dates can’t be translated (though, of course, they may display differently or be in different time zones). Similarly, it would make the Editor’s life much easier if they only needed to add an image once and that this was then shared between all variants of the event. So there would be no need to enable the “Allow vary by culture” checkbox on Image and Date as these are “shared” properties across all languages.
What Could Go Wrong?
This sounds great doesn’t it? We have made our editors lives much easier and we have also ensured that the date is consistent across all variations of the event - there’s no chance that an editor supplies the wrong date in one version of the event. We have also made it much easier to manage the Event image - if it needs updating it only has to be updated once. Fantastic!
But, if we haven’t enabled “Allow vary by culture” on the Date and Image, in which language variant do they reside? Where are they set (they’re mandatory after all)? Well, the answer goes back to when we created our Languages - you always have to supply a default language. So, for example, if we set the default language as English then it would be in this content tree that you would need to first create the Event - setting it’s Date and Image properties. And, because these are mandatory properties, you have to set them before you can publish this page in any language.
Can You Just...
Yep, those three little words that fill a developer's heart with dread! “Can you just…”
Now, imagine you’ve completed the site, deployed live and your client is really happy with their new shiny multilingual site. You are one happy developer, right?
But a couple of months later you are informed the client has made a “small” request. They now need to create a new Event which is only relevant to their German audience, so they “just” want the event to appear in the German site and not appear on the UK or other language sites…
Seems an innocuous request, right? But if you’ve been paying attention you’ll realise the problem we now have. English is the default language. It’s where the date and image are set for every Event (where they have to be set as they’re mandatory). Oh dear. Our One to One architecture just doesn’t really allow for this.
So what can you do?
Well, you could tell your client that it just isn’t possible. But, c’mon, we all know clients aren’t going to be happy with that. After all, it seems like such a simple request, doesn’t it?
So, assuming you can’t or won’t tell them it’s not possible, then you have to start thinking about unravelling your architecture. For a start, if they want to create Events in German but not in English then you really have no choice but to make the Date and Image properties “vary by culture”. This means you will now be able to add the Date and Image to the German version without requiring you to create a UK version first. Of course, this immediately does away with the whole point of why we made them invariant in the first place - convenience for the editor!
But you also want to ensure that every Event has a Date and an Image - so this means they have to be mandatory. And now you have another problem - all your existing events will suddenly require you to set the date and image for each language as they are mandatory! Oh dear, this is getting worse. Are you going to have to tell the client they now have to go through all the events and re-add the date and image for every language? That is not a conversation you want to be having...
But, hang on, what if we just use inheritance? Genius! You recall that when you create a language in Umbraco you can set it so that it “falls back” to another language. So what happens if we make German “fall back” to English if no content is set? That means that the client wouldn’t need to add an Image or Date for events where it was already set in the default language version - we can just “inherit” it. The developer in you feels just a little bit smug at coming up with this great solution!
Fall On Me
But wait… The first thing we need to realise is that falling back to a language doesn’t happen automatically. Sure, we can tick the box in the Language settings, but this doesn’t in itself change anything. If you want to fall back to a value you need to explicitly specify this when accessing the property. This is covered in the documentation, but to recap you need to use the following slightly arcane syntax in a view:
@Model.Value("image", "fr", fallback: Fallback.ToLanguage)
This will check if there is an image set for the language with the ISO language code “fr” (French) and, if there is not, fall back to our default language (which, in this case, is “en” or English).
If you are using Models Builder you’ll need to code it like this:
@model.ValueFor(x => x.Image, "fr", fallback: Fallback.ToLanguage)
Or if you are using an older version of Umbraco 8 then it would be:
@model.Value(x => x.Image, "fr", fallback: Fallback.ToLanguage)
As you can see, you’ll need to start updating your code for this to work. And all those nicely strongly-typed Models Builder models are suddenly not quite so nice to work with. Luckily there is a work-around for Models Builder which you can use, thanks to some great work by Jeroen Vantroyen - have a read of his article Umbraco - Enabling Language Fallback by default for the lowdown if you are interested.
Note: You can access the current "Culture" via the Culture property exposed in every Razor view.
But we have a bigger problem than that…
Let’s recap. If you want to fall-back to the default language for a property you will need to leave that property empty (ie. not set a value). Do you see the problem yet? Yep, that means we can no longer make the property mandatory (required). In other words, to use inheritance to solve our initial problem we now need to make it so both the event Date and Image are no longer required properties. This is not good because those are essential to the “referential integrity” of our data - you can’t have an event without a date, after all!
So, basically you have a choice to make - do we want it to be mandatory or do we leave it empty so it can fall-back to another language? ‘Cos you ain’t having both!
As you now see, the choices you make at the start of a project can greatly restrict the flexibility you have down the line. This is why it’s so important to get the client requirements at the start so you can consider these problems before they come to bite you. You can have flexibility at the price of convenience or convenience at the price of flexibility - but not both. Be warned! One to One is great...until it isn’t.
The Repeatable Content Problem
As Jane Austen once said, “It is a truth universally acknowledged, that a website of any degree of complexity will require repeatable content.” She was a wise woman.
By repeatable content I mean any content that is defined as Element Types, such as those you will find in Nested Content, Stacked Content, The Grid or the new fancy-pants Block List Editor. I find that in most non-trivial sites I build that the majority of content is now composed using such an editor, so this is no minor thing.
So why is this a problem? Well, the plain fact is that you can’t “vary by culture” an Element Type. Well, actually you can, because the interface quite happily lets you do this - but it doesn’t actually do anything. The problem is the way that Element Types are stored and serialised as JSON. As a reply to this open issue on Github puts it:
“This can't be fixed without completely rewriting the way element types are stored, as currently all data is stored as JSON in a single property. There's a RFC to change this, but the feature to keep variants in sync is explicitly stated as being out of scope: umbraco/rfcs#24.”
In other words, Element Types have no way of storing an associated culture currently. Maybe in the future this will change if RFCS 24 comes to fruition, but as we all know, telling a client something may happen in future is not likely to placate them.
So What Happens?
To understand the problem, let’s imagine we have to build a very simple Image Gallery component for a page using Nested Content. Our requirement is to have an image, a heading and a short caption. So we’d create an Element Type with three properties: Heading, Image, Caption and then create a Nested Content data type that lets you add, say, up to 20 of these elements within a page. We’ll call this property Image Gallery and we’ll vary it by culture.
Now, ideally what our client wants is for the images to be added just once - so they are shared across all language variants. But they do require the Heading and Caption fields to be translated into all available languages.
However, this isn’t currently possible as we can’t “vary by culture” the Heading and Caption with the element types. Sure, you can set the Image Gallery property (that uses the Nested Content datatype) to be “varied by culture” but this treats it as a single property. The individual elements that make it up can’t be varied.
In other words, we have to tell our client that they will need to add the images to each language separately along with the translated Heading and Caption. They can’t be shared. And, if like me, your site content is mostly comprised of this type of repeatable content then you will find one of the big advantages of Umbraco 8 has just been diminished.
Copy'n'Paste to the Rescue
Luckily all is not quite as bad as it seems. The interface for Nested Content (and other editors) has been updated to allow you to copy and paste element types. This means you can go into split view in Umbraco and copy the nested content from one language to another fairly easily (though this feature isn’t inherently obvious if you are not aware of it). So, in our example, you could copy the entire Image Gallery from the English site to the German site in a couple of clicks and then you’d have the same images - all you would need to do is edit them and translate them. This still leaves you with a lot of duplicated content, but it’s still better than having to add it all again manually.
Searching with Examine
Another issue you are likely to encounter with Umbraco 8 is how Examine indexes multilingual content. By default you will just have one External Index which contains content for all available cultures.
If you look at an Examine index for Umbraco 8 you’ll also notice that each indexed property now has the culture appended to its field name. For instance, if you have a property called ‘heading’ it will now become ‘heading_en_gb’ or ‘heading_da_dk’ etc. This allows you to distinguish between content from different cultures, but does fundamentally change your search queries. If you are relying on a 3rd party package for search that isn’t aware of this it's likely not to work.
You’ll also see some special fields added to the index that let you know it varies by culture and which versions are published. Something like this:
__Published_en-gb y
__Published_da-dk y
__VariesByCulture y
I won’t go into how to write a search for Umbraco 8, as this is a topic in its own right. Instead, I’d refer you to the article ‘Examine in Umbraco 8’ by Paul Marden published by Skrift. It touches on accessing multilingual properties and provides a good starting point. The main thing is to be aware of this before embarking on building a site.
Picking Content
In any site you will often have content pickers - these can be the standard Content Picker data type or maybe you are more hip and use the snappily named Multi Node Tree Picker. Either way, you need to be able to select content from the site you are editing.
You would expect that if the editor has set the content tree to, say, the German site then the content picker would just show you pages in that particular site in that language. However, this doesn't always seem to be the case in Umbraco 8 - quite often it will show you the tree in the default language (eg. English) regardless of what language the content tree is set to. There are open issues for this but it's another little gripe you may well encounter.
Partial Caching
For optimum performance you will want to cache many of your partials. However, in a multilingual site you also want to ensure that you don't cache, say, the French content and show that on the English site. Before Umbraco 8.8 came along this was quite easy to do by mistake (and because partials aren't cached when debug mode is enabled, you might not notice until you deployed live!).
Take for instance this innocuous statement to cache a partial for an hour:
@Html.CachedPartial("MyPartial", Model, 3600)
Now imagine you have a site in English and French. If the very first request for the page with your cached partial is from the French version then the French content would be cached and subsequently shown on every further request - to both language sites. Those poor English people wouldn't have a clue what was going on! :) Likewise, if the first request was to the English language version it would be cached in English - for both sites. This could be a very difficult bug to spot!
Luckily the amazing Marc Goodson submitted a PR to "Add the current culture to the cache key for the CachedPartial Html Helper" which fixes this issue in Umbraco 8.8 and later. However, if you are using an older version of Umbraco you can achieve the same effect using the contextualKeyBuilder parameter that allows you to specify a custom cache key:
@Html.CachedPartial("MyPartial", Model, 3600, contextualKeyBuilder: (m, v) => Culture)
Bonus Content!
OK, if you’ve gotten this far you’ve probably realised that whilst the multilingual features in Umbraco 8 are very welcome they are specifically geared toward One to One sites where the assumption is that you will always have a default language, with the other sites being translated from this default language. There are issues with falling back to default language, especially if you use a lot of repeatable content.
But there are other advantages you will find in Umbraco 8 that can make multilingual sites much nicer.
Switching Languages
One common requirement of any multilingual site is to switch between the various languages on offer. Sure, you can use fancy tricks like geolocation or checking the browser language header to send someone to the “correct” site, but ultimately you still need a way of choosing manually. This is why nearly all sites will have some kind of switch language menu.
Ideally this switching will work at the page level. So, for instance, if someone lands on a German event page and switches the language to English they will be taken to the exact same event page but in English.
In multi-tenancy sites this was always very difficult, if not downright impossible. This is because there is no direct relationship between the pages - each page is completely separate and doesn’t have any “knowledge” of its variants. Sure, you can try and be clever and use the Relationship API in Umbraco to make these connections, but when you have more than two languages this gets very problematic to enforce. In most multi-tenancy sites I just had to leave it so the switcher took you to the home page every time.
However, in One to One sites there is that internal relationship and we can leverage this fact to make a nicer language switcher that keeps the visitor on the same page even when they switch language.
To do this we take advantage of the new Cultures property on IPublishedContent. This allows us to loop over all the available cultures (languages) for a document. This means that when we are on any given page we can access all the other page variants, which we can use to create our language selector. We can also be a bit clever and check if a particular language doesn’t exist then we can make our selection default back to the home page for the site. This ensures our language picker always works even if a page doesn’t have all the variants created for it. We can also add some (optional) caching to further improve performance.
So, without further ado, here’s a couple of code snippets that can achieve this:
The first is a razor partial that displays a language picker dropdown using Bootstrap 4 styling (you can obviously change this to use your own framework or styling):
@inherits UmbracoViewPage
@using Diplo.Web.Language
@*
Example Language Switcher using Bootstrap 4
*@
@{
var languages = Model.GetCurrentLanguageList(Culture, AppCaches);
var selected = languages.FirstOrDefault(x => x.Current);
string currentName = selected != null ? selected.Name : Culture;
if (languages == null)
{
return;
}
}
<li class="nav-item dropdown">
<button class="btn nav-link dropdown-toggle" id="languageDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">@currentName</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="languageDropdown">
@foreach (var item in languages)
{
<a class="dropdown-item d-block @Html.If(item.Name == currentName, "active")" href="@item.Url">@item.Name</a>
}
</div>
</li>
This partial code consumes a couple of helper classes / extension methods that generate the list of available URLs based on the current passed in page. It also uses Caching to cache the root languages to keep things snappy! Here it is:
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web;
namespace Diplo.Web.Language
{
public static class LanguageExtensions
{
public class CultureUrl
{
/// <summary>
/// The culture string eg. en-GB
/// </summary>
public string Culture { get; set; }
/// <summary>
/// The page URL
/// </summary>
public string Url { get; set; }
/// <summary>
/// Gets the short name eg. GB
/// </summary>
public string Name => this.Culture?.Length > 2 ? this.Culture?.Substring(3, 2).ToUpper() : this.Culture?.ToUpper();
/// <summary>
/// Gets whether this is the currently selected page
/// </summary>
public bool Current { get; set; }
}
/// <summary>
/// Generates the list of languages for the page dropdown. If there is a parallel page using same culture then this is used, otherwise it falls back to the root URL.
/// </summary>
/// <param name="currentPage">The currently viewed page</param>
/// <param name="currentCulture">The current culture</param>
/// <returns>A list of languages and cultures</returns>
public static List<CultureUrl> GetCurrentLanguageList(this IPublishedContent currentPage, string currentCulture, AppCaches caches)
{
var home = currentPage.Root();
var rootCultures = GetRootCultures(home, caches);
var pageCultures = currentPage.Cultures.Select(x => x.Value);
var mapped = new List<CultureUrl>();
foreach (var root in rootCultures)
{
var pageMatch = pageCultures.FirstOrDefault(c => c.Culture == root.Culture);
CultureUrl mappedPage = null;
if (pageMatch != null)
{
string url = currentPage.Url(pageMatch.Culture);
if (url != "#")
{
mappedPage = new CultureUrl()
{
Culture = pageMatch.Culture,
Url = url
};
}
}
if (mappedPage == null)
{
mappedPage = new CultureUrl()
{
Culture = root.Culture,
Url = home.Url(root.Culture)
};
}
mappedPage.Current = mappedPage.Culture.InvariantEquals(currentCulture);
mapped.Add(mappedPage);
}
return mapped;
}
private static IEnumerable<PublishedCultureInfo> GetRootCultures(IPublishedContent currentPage, AppCaches caches)
{
return caches.RuntimeCache.GetCacheItem("RootCultures", () => currentPage.Cultures.Select(x => x.Value).ToList());
}
}
}
Hopefully the above code examples will be enough to get you started creating your own picker :)
Generating HrefLang Tags
Another important thing for any multilingual site is to ensure it has Hreflang tags. These are a special type of metadata (added to your document HEAD) that help search engines understand the relationship between the different language pages in your site.
You can quickly generate these tags like this:
@foreach (var culture in Model.Cultures)
{
string url = Model.Url(culture: culture.Key, mode: UrlMode.Absolute);
if (!string.IsNullOrEmpty(url) && url != "#")
{
<link rel="alternate" href="@Model.Url(culture: culture.Key, mode: UrlMode.Absolute)" hreflang="@culture.Key" />
}
}
You’ll notice I check if the URL is both empty and also not equal to the “#” as sometimes you find the API returns this rather than an empty string. C'est la vie! :)
You also will spot I’m using the overload for the Url() extension method on IPublishedContent that allows you to pass in the culture and specify you want an absolute URL returned.
Note: You should always use the Url() extension method going forward as the Url property is now deprecated.
Conclusion
So should you use Umbraco 8 for One to One multilingual sites? That’s not something I can answer for you as each site will be different and each client will have their own requirements. Hopefully, though, with the help of this article you’ll be able to consider all the angles and ask the right questions before starting coding! (I know that is hard).
I also personally hope that Umbraco gets a little more love in this regard - the wizards at HQ have already solved many difficult problems and created a very slick editing experience - but there is more that needs to be done to make it more flexible. I’m sure it will get there!
Dan Booth
Dan is on Twitter as @DanDiplo