A custom localization provider for EPiServer 7, revisited

Not really a big post, but I got some requests to update the provider for a newer version EPiServer. So version “2” is compatible with 7.7.1 and above. I also added on request an option for automatic translations through an external provider. I chose Bing, as it has quite enough free translations a month to do the job.

So when you publish your translation, and you have added two keys to the appsettings (“localization.bing.clientsecret” and “localization.bing.clientsecret”), the translations will be added for the other languages you have enabled.

This is done by getting the enabled languages, looping through them and create a language branch for the languages not yet existing for the item.

ILanguageBranchRepository languageBrancheRepository =
                ServiceLocator.Current.GetInstance<ILanguageBranchRepository>();

 List<LanguageBranch> enabledLanguages = languageBrancheRepository.ListEnabled().ToList();

foreach (LanguageBranch languageBranch in
                enabledLanguages.Where(lb => lb.Culture.Name != page.LanguageBranch))
            {
                this.CreateLanguageBranch(page, languageBranch.Culture.Name);
            }
private void CreateLanguageBranch(PageData page, string languageBranch)
        {
            // Check if language already exists
            bool languageExists =
                this.ContentRepository.GetLanguageBranches<PageData>(page.PageLink)
                    .Any(p => string.Compare(p.LanguageBranch, languageBranch, StringComparison.OrdinalIgnoreCase) == 0);

            if (languageExists)
            {
                return;
            }

            TranslationItem translationItem = page as TranslationItem;

            if (translationItem != null)
            {
                TranslationItem languageItemVersion =
                    this.ContentRepository.CreateLanguageBranch<TranslationItem>(
                        page.PageLink,
                        new LanguageSelector(languageBranch));

                languageItemVersion.PageName = page.PageName;
                languageItemVersion.URLSegment = page.URLSegment;

                string translatedText = this.BingTranslate(
                    translationItem.OriginalText,
                    page.LanguageID.Split(new char['-'])[0],
                    languageItemVersion.LanguageID.Split(new char['-'])[0]);

                if (translatedText == null)
                {
                    return;
                }

                languageItemVersion.Translation = translatedText;

                if (!string.IsNullOrWhiteSpace(languageItemVersion.Translation))
                {
                    this.ContentRepository.Save(languageItemVersion, SaveAction.Publish, AccessLevel.NoAccess);
                }
            }
            else
            {
                PageData languageVersion = this.ContentRepository.CreateLanguageBranch<PageData>(
                    page.PageLink,
                    new LanguageSelector(languageBranch));

                languageVersion.PageName = page.PageName;
                languageVersion.URLSegment = page.URLSegment;

                this.ContentRepository.Save(languageVersion, SaveAction.Publish, AccessLevel.NoAccess);
            }
        }

As always the code is on GitHub or available through NuGet when it’s approved.

12 thoughts on “A custom localization provider for EPiServer 7, revisited

  1. Hi, really useful module. I was debugging a client site using your module in a load balancing environment, when I started zeroing in on the remote event handling in your module. Then I thought I might check the current version, and lo and behold, a change was in place for the area that I had identified as ‘risky’.

    This fix in question is https://github.com/jstemerdink/EPiServer.Libraries/commit/b9a604871fe33c49d4734c0169c63acae7843a83#diff-f2426516581aeaa25aabb474b0deef23 .

    My question is, can you please specify just what symptoms you were made aware of that led you to the fix you now have in place? I would like to match this against what I’m seeing, since I can’t really be sure that the issue in your module really is at fault for what I’m seeing…

    Like

    1. Hi,
      If you read the comments on the “original” post you can read what lead to the change.
      The GetString override I mention in one of the last comments is in the 7.7+ branch on GitHub.
      Small question, what are you seeing?

      Like

      1. Sorry, I missed the thread in the original post. I found the troublesome code independently when auditing a solution, and then looked at it again due to a situation where some remote events appear to be lost in a load balancing environment. I have not positively traced it to your localization provider, but it’s the only custom code even close to the remote events handling…

        The ‘original’ code, where you listen to the remove from cache event, is defintely a problem for a variety of reasons some of which are adressed by the recent fix in October on GitHub.

        The reason I think this might cause the apparent loss of remote events is because EPiServer will issue a large number of various forms of remove from cache remote events, and each of them will start executing the load-reload cycle. This will for one thing cause massive load on the system, since it needs to read and process a potentially large number of pages, and it will also expose the code to a large number of race conditions potentially causing crashes and inconsistencies.

        There are still some race conditions and other things that should be fixed for it to work correctly in a high-load (not necessarily load balancing environment, it’ll fail sometimes in single-server environment too). Would you be willing to accept a pull request?

        Like

      2. Am not quite sure I follow. The remove from cache events will be always triggered on e.g. a publish. The fix was to reload the translations in the provider on other servers only when a translation or it’s container is updated. Hence the custom Event en Raiser ids. The hooks will only be executed for a Translation or it’s container and the custom event is only triggered then. But I may have overlooked something. I never experienced hooking into e.g. a published event causes a loss of remote events though.
        But of course I’ll accept a pull request if it fixes issues with the code, why wouldn’t I?

        Like

      3. Ok, great. I’ll be working on this later in the week.

        As you say, the current fix reduces the problems by only reloading when a translation page is affected. The problem I’m seeing with the original code is that it will trigger remotely many times for every change in the page tree. Not only for pages not affected, and up to 10 times per modification because the RemoveFromCacheEventId with different content may be sent many times for a single operation. The initial large problem was that originally you did not hook into the page events, but the cache removal events for remote invalidation.

        The current fix is much more well behaved, but there’s at least one race condition during the unload-reload cycle that may be triggered, both remotely and locally. There is no guarantee that a different thread will not try to access the provider during this cycle, causing unknown effects. The thread lock added in the fix does not protect against this, it only synchronizes the reload cycle itself. The simple case is that a thread calling during that time will simply not find the provider in the LocalizationService.Providers collection. I have not had time to analyze it fully for other issues, but I’ll try to fix it for all race conditions.

        If I’ve misunderstood something, do let me know. I’ve only had a quick peek at the code.

        Anyway, you can check out the result when I’m done later this week if all goes as planned.

        Like

      4. Accessing the provider by a different thread cannot be prevented I think, but maybe I’m wrong.
        That’s why in the provider itself I did an override for the GetString method that gets called when looking for a translation, this has not been merged into the master yet btw.
        An InvalidOperationException will be thrown when accessing the translations during reloading. The override should handle it gracefully, at least not throw an exception. But there could be better ways. Looks like you got a lot more knowledge about threading issues, so am happy to learn. Thanks for helping in making this a better module 🙂

        Like

  2. Hello Jeroen,

    I just installed the latest (2.0.0) version of the provider in an EPiServer 7.5 solution (Nuget: id=”EPiServer.CMS” version=”7.15.0″).
    When testing it seems like the TranslationContainer is never found.
    In the TranslationFactory class [ line 589 at http://bit.ly/1wweFnj ] ContentReference.StartPage seems to always be null, hence causing the provider to have zero AvailableLanguages and no translations.

    I already tried with both:
    * A property of type PageReference named “TranslationContainer” in the StartPage.
    * Creating the TranslationContainer page directly underneath the StartPage.

    Is there any step that I might be missing?

    I am wondering if there are implications that we haven’t considered in regards to this EPiServer 7.5 feature:
    http://world.episerver.com/Blogs/Johan-Bjornfot/Dates1/2013/12/Multisite-feature-in-EPiServer-75/

    Best Regards,
    – Wac

    Like

    1. Hello Jeroen,

      I found this paragraph in the previous link, and was wondering if that is what I was experiencing:

      If no wildcard site is defined a default SiteDefinition is returned where common settings like e.g. RootPage is set but StartPage will be ContentReference.EmptyReference and SiteUrl will be null.

      Also worth noticing is that there is one initialization for the application where all sites are initialized (unlike before where the initialization where for a specific site since each site run in a separate AppDomain). So if you have some custom initialization module that are site dependent you could use SiteDefinitionRepository.List() to get a list of all defined sites.

      So I added * in the host mapping for one of the sites and indeed got the translation working.
      It seems like the current implementation of the Translations Provider is not compatible with the Multisite feature, as it will load the provider of only one of the sites.

      The easiest way to solve this perhaps, is to have the translations at a global level (underneath the Root), rather than underneath the ContentReference.StartPage.
      The ideal way to get around this would be to somehow recognize at a request level which is the needed provider, and deactivate the other ones somehow. I will take a look at this and let you know my findings.

      Cheers,

      Like

    2. Hi,
      You are absolutely right, sorry about that. I have changed the code to look for the container beneath the root first, which is the only option in a multi site setup I think.
      Do not know when it will show up in NuGet though. EPiServer has requested a namespace change also, to avoid confusion where the package comes from.
      I also seperated the Bing service from the main functionality.
      You might wanna have a look at a new version I have been working on, which uses a memory provider instead of the xml provider.

      Like

  3. Awesome thank you,

    I will take a look. For now, for this installation I will create a site with a * mapping that holds a TranslationContainer property pointing to a container underneath the Root.

    Whenever the new nuget becomes available, the required steps would be to remove the mock site and let it be.

    In regards to “which is the only option in a multi site setup”, I think another option is the following:
    – On initialization, run through the site definitions
    – Initialize as many providers as needed (add an ID to associate the provider with the site definition)
    – Override the provider Translate method so that:
    — It checks the Request ContentReference.StartPage (at this point, since it is in the context of a request the StartPage is available)
    — Compare the StartPage with the one associated with the provider.
    — If it doesnt match, then just ignore it and continue with the next provider.

    Not sure which option will offer better performance, certainly the one stated above seems to offer greater flexibility for the Multisite environment though.

    Cheers,
    – Wac

    Like

    1. That would be a possibility, but if it would be good performance wise, I am not sure.
      But if you would really have different translations for different sites, it would also mean different templates, with different keys for the translation?

      Like

Leave a comment