Database persisted translations with edit tools for language file resources in globalized EPiServer websites

A few years back I wrote a small tool allowing web administrators to update EPiServer CMS 6 R2 language files by themselves without the help of their website developers. There were of course a few downsides to this, such as updated deployment procedures as not to lose any new translations. I took a few hours and made a new one for EPiServer 7.5 that works by storing translations in the database instead, removing any concern of maintaining langauge files while deploying new code. Creds should also go to Jill Karlsson for providing awesome frontend support. Source code for this functionality at GitHub.

Note: There is now a migration tool available to ease the creation of initialization classes from the legacy EPiServer language files.

Allowing webeditors to dynamically update language translations in a global context

This functionality is based on resource keys, just as EPiServer’s language files are. For instance the key /some/sample/key in the below language file sample would return the string Some text for the IETF language tag en.

<?xml version="1.0" encoding="utf-8"?>
<languages>
  <language name="English" id="en">
    <some>
      <sample>
        <key>Some text</key>
      </sample>
    <some>

These resource keys are now imitated in the database to support for fallback to the EPiServer language files in the case that a resource text could not be found. The translations in this functionality may be edited through one of the two supplied edit tools; the EPiServer global menu tool, or the plug-in asset for a content type specific context.

EPiServer global menu tool for updating database persisted translations.

A large overview of all the translations for the current language (localhost:1234 is my English sample site, and localhost:1233 is my Swedish) may be seen in the global menu tool in the image above. They are sorted on the content type to which they belong, while translations without any specific type goes under Global translations; the latter one is only editable through this tool.

By altering a text and clicking the corresponding Update link the new string translation is stored in the database together with an updated changed date as well as the current user’s name.

Updated text in EPiServer global menu language tool.

Allowing webeditors to dynamically update language translations on content types

To access the translations in a content type based context, there is another tool accessible in EPiServer’s right hand asset pane in the EPiServer edit mode. What is shown in this area will change depending on the content that the webeditor is currently viewing. The image below displays a page of type ArticlePage being selected in the EPiServer page tree, and the corresponding translations in the tool.

EPiServer plug-in asset tool for updating database persisted translations.

The same functionality is available here; webeditors may update translation texts and save them in the database without worrying that they may be overwritten in a language file during deployment of new code.

Intercepting EPiServer’s LocalizationService making it use the database instead of language files

The way that this functionality works is not a complicated one, it takes advantage of a decorator pattern intercepting EPiServer’s language locator service and redirecting it to our database persisted texts. I use a wrapper around EPiServer’s LocalizationService as below.

ILocalizationServiceWrapper.cs

public interface ILocalizationServiceWrapper
{
  string GetString(string resourceKey);
}

LocalizationServiceWrapper.cs

public class LocalizationServiceWrapper : ILocalizationServiceWrapper
{
  public string GetString(string resourceKey)
  {
    return LocalizationService.Current.GetString(resourceKey);
  }
}

This wrapper is what I use all over the website in order to get language translations from EPiServer’s language files. The new functionality implements a decorator for this as below.

LocalizationServiceDecorator.cs

public class LocalizationServiceDecorator : ILocalizationServiceWrapper
{
  private readonly ILocalizationServiceWrapper _inner;
  private readonly IContentLanguageWrapper _contentLanguage;
  private readonly ITranslationRepository _translationRepository;

  // Constructor with dependency injection removed
  // for less noise.

  public string GetString(string resourceKey)
  {
    return FromDatabaseOrDefault(resourceKey) ?? _inner.GetString(resourceKey);
  }

  private string FromDatabaseOrDefault(string resourceKey)
  {
    var ietfLanguageTag = _contentLanguage.PreferredCulture .IetfLanguageTag;
    var translation = _translationRepository .TranslationFor(resourceKey, ietfLanguageTag);
    return translation == null ? null : translation.Text;
  }
}

The _inner variable in this case is the original LocationServiceWrapper that uses language files to locate translations. Interesting parts here is mostly in the GetString method; if the translation cannot be found in the database, the request is passed on to the old functionality instead as a fallback. So, the next step is to make StructureMap inject our decorator instead of the original localization service whenever it’s requested (either by constructor injection or by using EPiServer’s ServiceLocator). This is done through a simple StructureMap Registry

LocalizationRegistry.cs

public class LocalizationRegistry : Registry
{
  public LocalizationRegistry()
  {
    For<ILocalizationServiceWrapper>()
      .Use<LocalizationServiceDecorator>()
      .Ctor<ILocalizationServiceWrapper>() .Is<LocalizationServiceWrapper>();
  }
}

Basically, what it’s saying is that whenever someone requests an instance of the interface ILocalizationServiceWrapper, supply an instance of the decorator. However, if the ILocalizationServiceWrapper is requested in the constructor of the decorator then use the original LocalizationServiceWrapper instead.

This, together with the proper set up of StructureMap, is all that is needed for intercepting the translation functionality.

Initializing new translation texts without language files

Since we want to get rid of the use of language files, we need a new way of initializing translations in the database. Sure, we could probably script them, or create some sort of admin tool for this, but I’d rather do things automatically from code than add manual steps.

For now, the initialization process involves inheriting the IInitialTranslations interface in language specific translation classes; for now, since it may be changed to support strongly typed access later on. The interface requires you to implement an IETF language tag property telling which language that you wish to add translations for, as well as an array of translation groups. Each translation group corresponds to a content type; below is a sample of how a translation group is created for ArticlePage.

En.cs

var articlePage = new TranslationGroup
{
  // Translations editable on pages of type ArticlePage.
  ContentTypeName = typeof(ArticlePage).Name,
  Translations = new[]
    {
      // Initial english article page translations goes here
      new Translation{ResourceKey = "/some/sample/key", Text = "English demo text"},
      new Translation{ResourceKey = "/some/other/sample/key", Text = "Another English demo text"},
    }
};
initials.SetupAndAdd(articlePage, IetfLanguageTag);

The easiest way to add translations or new languages in this solution is copying one of the existing (En.cs or Sv.cs) language initialization classes. Note that the translations used by the tool itself are included in these classes.

To initialize global translations, or translations that are used on several content types, just omit the ContentTypeName property when creating the TranslationGroup. The SetupAndAdd method will then revert to the default one; ContentData.

Configuration of the EPiServer translation tool

The TranslationInitializer is an initializable module that is run when the application starts. The purpose of this is to look through your language initialization classes and add any new entries to the database, as well as remove orphan translations in the case that you removed entries from the code. This initializer need only be run once after you deploy code with changes to any class implementing the IInitialTranslations interface, which is why there is a new configuration section that may be added to your web.config transformation files.

web.config

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="LanguageToolConfiguration"
             type="{your namespace}.LanguageTool.Infrastructure .Configuration.Configuration, {your assembly}" />
  </configSections>
  <LanguageToolConfiguration InitializeNewTranslations="True"
                             RemoveOrphanTranslations="True" />

There are two things that you will need to add in order to configure the language tool initializer; the first is adding a section called LanguageToolConfiguration (line 4-5) to the configSections part of the configuration file, and the second being to add the config itself (line 7).

These attribute settings are then picked up by the Configuration class in the tool and passed on to the initializer. If you’d like to read about how to add custom configuration sections, Joel Abrahamsson has written a short article about it.

Things that you need to do, or may want to change

So, there are a few things that you may want to see to if you decide to try this language tool out. First off, add the configuration section as described above. Second, there are a bunch of jQuery and CSS in the Static directory of the tool; you may want to put them wherever you keep yours. Also the _AssetLayout.cshtml and _ToolLayout.cshtml files both contain script tags referencing //code.jquery.com/jquery-1.11.2.min.js. This is due to me being lazy, but I guess you may want to remove it from there. You may want to replace the layout files with your own ones if you have such for EPiServer tools. And lastly there is the database issue; this solution currently uses the EPiServer DynamicDataStore (DDS) for persisting translations. Of course it would be working just fine if you’d like to go with that, but if you keep your own custom database separate from the EPiServerDB, that may be a good place to put this data. To switch place of storage you would need to make changes to the domain object itself (Translation class) as well as the translation repository (TranslationRepository class).

Planned features

Other features planned for this tool is on-page editing of the translations to prevent the need for a separate tool, and promote in-context editing for the webeditors. Also easier access to the transations for the developers, to avoid having to use resouce key strings and rather access the translations in a strongly typed fashion. Automatic migration from legacy language files to the new initialization classes is also in the pipe. The migration tool is found in a later article.

Leave a Reply