A custom localization provider for EPiServer 7.

The problem
Who hasn’t been editing the xml files in the lang folder. With or without custom plugins. When a client wants another localizable text, or a different translation for something, a new release was needed. If you used a plugin to edit the files and the site was load balanced, replication almost always was a problem. It’s bit better in EPiServer 7, but still… most of the time a new release was / is needed.

What I needed
I wanted Editors to be able to change translations on their own, with no fear of load balancers / file replication making a mess of things. So it needed to be done within EPiServer itself, not with a plugin in admin mode, but in the content tree.

The solution
I created it first for EPiServer 6, but with the new, pluggable, localization providers it became much easier to implement. No custom control needed to display the translation for example.

So for EPiServer 7 I created three PageTypes.

  • A “TranslationContainer” for normal translations
  • A “CategoryTranslationContainer” for translations of Categories (as the xml for those translations are slightly different from a normal translation)
  • A “TranslationItem”

To be able to use the translations created within the website, I created a “Translation Provider”, which is based on the XmlLocalizationProvider from EPiServer 7. The provider gets initialized through an IInitializableModule, so no configuration is necessary, and gets its data from the “TranslationFactory”.

You start with creating a “TranslationContainer” underneath the StartPage, or wherever you want. If you want to put the container somewhere else, you will need to set a property of type PageReference on the StartPage, named surprisingly “TranslationContainer”.

The code
Read the SDK for the basics for initializing a localization provider from code.

I have commented the code extensively, but I will explain a few things that needed implementing.

Except from the basic things as described in the SDK you will need to reload the provider after initializing, publishing, moving and deleting. The xml that gets generated will change on these events, so it needs to be reloaded. Kinda like a file dependency.

In the initialization module I needed to attach some events.

// Initialize the provider after the initialization is complete, else the StartPage cannot be found.
context.InitComplete += this.InitComplete;

// Attach events to update the translations when a translation or container is published, moved or deleted.
DataFactory.Instance.PublishedPage += this.InstanceChangedPage;
DataFactory.Instance.MovedPage += this.InstanceChangedPage;
DataFactory.Instance.DeletedPage += this.InstanceChangedPage;

In the attached events I retrieve the translations as an the xml structure, in the same format as a lang file, then I load the generated xml into the LanguageDocument from the provider, which is where a XmlLocalizationProvider stores it’s data.

string translations = TranslationFactory.Instance.GetXDocument();
byte[] byteArray = Encoding.Unicode.GetBytes(translations);

using (MemoryStream stream = new MemoryStream(byteArray))
{
	 this.Load(stream);
}

The xml is created by looping through the translation containers and the translation items for each language the main translation container is created in. Have a look at the code over at GitHub, if you’re interested.

It will look something like this:

<!--?xml version="1.0" encoding="utf-16"?-->




        Second tag


        First tag



      Translation One





        Eerste tag



      Tekst een


A few things to keep in mind.
When you create a translation the OriginalText is the key, and can only be set on the master language. To keep it simple for the editors just name them normally. To reference this use the name without spaces and special characters, all lower case, this to be in line with the lang files.  See “Bonus” how to get the key in an easy way,

So if the Name of the container is e.g. Sub Node, the key in the xml will be subnode. The same applies to  the translation items, Text One will be textone.

For translations for Categories it is slightly different. You create a “CategoryTranslationContainer” beneath the main container, this will trigger a different rendering of the xml. You can organize them with sub containers, but in the xml those sub containers will not be used. The value of the “OriginalText” property needs to be exactly the same as the name of the Category you want to translate.

I found that the translations are not always shown in Edit mode, but when switching to preview mode for the language you get the correct language. This probably has something to do with the language of the interface you use. It’s the same if you create a xml file in the lang folder.

You can use these translations like any other translation. E.g.


If you want to display a translated category on your page use the the LocalizedDescription property of the Category.

foreach (Category cat in this.CurrentPage.Tags.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(category => Category.Find(int.Parse(category))))
 {
   	    this.Response.Write(cat.LocalizedDescription);
 }

“Bonus”
On the “TranslationItem” I have added three properties you can use on a template, if you create one for a translation that is. You will not use it in the site, but it can display a few things using those properties.

· MissingValues: the languages that have no translation yet.

· LookupKey: the key to use in e.g. the Translate WebControl.

· TranslatedValues: a dictionary containing the language and the translation.

foreach (string missingValue in this.CurrentPage.MissingValues)
            {
                Response.Write("Missing: ");
                Response.Write(missingValue);
                Response.Write("

");
            }

foreach (KeyValuePair<string, string> value in this.CurrentPage.TranslatedValues)
            {
                Response.Write(value.Key);
                Response.Write(": ");
                Response.Write(value.Value);
                Response.Write("

");
            }

Response.Write("LookupKey: ");
Response.Write(this.CurrentPage.LookupKey);

Requirements

  • EPiServer 7
  • Net 4.0
  • log4net

Deploy

  • Just compile the project
  • Drop the dll in the bin
  • Let the Editors handle their own translations. One less task to do.

The code can be found on GitHub.

31 thoughts on “A custom localization provider for EPiServer 7.

  1. Hello Jeroen,

    Great idea, we installed the package in one of our projects and is working like a charm for single server environments. However we found a “small” issue in a NLB scenario with cache invalidation configured through Event Management:

    If a page gets published the translations get updated only on that server. If a normal page gets published, the cache invalidation happens successfully.

    My guess is:
    When a translation page gets checked-in/changed/deleted in an edit server it triggers three actions:
    a) invalidate cache
    b) update translations
    c) propagate change.
    When a web-front server receives the notification (through remote-events), it invalidates cache however it does not update the translations.

    Can you please advise if the current library is already meant to handle NLB scenarios?
    If not, do you have any preference/suggestion on how to do so?
    I am more than happy to discuss the code update and make you a pull request if needed 🙂

    Cheers
    – Wacdany R.

    Like

    1. Ah sorry, did not take a loadbalancer into account.
      The translations are kept in memory by EPiServer and get (re)loaded on an application reset/recycle.

      In the Initialize method another event handler needs to be attached:

      Event removeFromCacheEvent = Event.Get(CacheManager.RemoveFromCacheEventId);
      removeFromCacheEvent.Raised += this.RemoveFromCacheEventRaised;

      The handler should execute the reload

      private void RemoveFromCacheEventRaised(object sender, EventNotificationEventArgs e)
      {
      // We don't want to process events raised on this machine so we will check the raiser id.
      if (e.RaiserId != CacheManager.LocalCacheManagerRaiserId)
      {
      this.ReloadProvider();
      }
      }

      I have no way of testing this on loadbalanced environment at the moment, so I do not want to upload a new version to NuGet yet, or would you be able to test this change? You can download a dll with these change here.

      Like

  2. Hello Jeroen,

    Thank you for your prompt response on this, we are coordinating a quick test with the new package in a NLB environment and will let you know the results as soon as we have them.

    Again thank you!

    – Wacdany R.

    Like

  3. Hello Jeroen,

    After replacing the dll we ran into a few assembly binding issues, after correcting those issues the Translation engine doesn’t seem to pick up the translations.

    When looking in depth in the assemblies we noticed that the updated (skydrive dll) assembly has a Neutral culture while the nuget package one has a “en” culture.

    Could this be causing the issue?

    Reference. Translation Item details:

    Nuget Package:
    Guid a691f851-6c6e-4c06-b62e-8fbc5a038a68
    Class name EPiServer.Libraries.Localization.Models.TranslationItem, EPiServer.Libraries.Localization, Version=1.0.0.0, Culture=en, PublicKeyToken=c5738ce365fc6356

    Updated assembly:

    Guid a691f851-6c6e-4c06-b62e-8fbc5a038a68
    Class name EPiServer.Libraries.Localization.Models.TranslationItem, EPiServer.Libraries.Localization, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c5738ce365fc6356

    Thanks for your help!
    – Wacdany R.

    Like

    1. I opnened the dll from DropBox and there is something strange with the dll after that. Am not quite sure what DropBox does with a dll, but it is not the same as the one I uploaded, so I uploaded the files as a zip file instead. Same location.
      The UnitTest run ok with the change I made. I tested it in my own environment. Still ok. And the event I added fires, but does nothing, as it is on the same machine, so that should work.

      What binding issues did you get btw?

      Like

  4. Hello Jeroen,

    Thank you so much for your help, and sorry for the late reply. I was checking this issue again, basically the binding issue can be easily solved by just removing the references to the previous dll in the actual C# projects and adding the new file to them.

    However, when trying to run under the new version I noticed that the translations are not being found.

    After turning on logging, the following was noticed:

    First:

    2014-03-31 15:56:44,073 [1] INFO EPiServer.Libraries.Localization.TranslationFactory: [Localization] No translation container specified.

    Double checking the current location of the container vs the latest algorithm to get the container.

    Looking at:
    * https://github.com/jstemerdink/EPiServer.Libraries/commit/6e6f3810b5a58c80df1315f86ef516ee5a2a58a0
    Class: EPiServer.Libraries.Localization/TranslationFactory.cs
    Method: GetTranslationContainer

    It seems like result of the lookup method :

    this.ContentRepository.Get(ContentReference.StartPage)
    .GetPropertyValue(“TranslationContainer”, ContentReference.StartPage);

    is not getting assigned to the variable _containerPageReference_.

    Hence, the TranslationContainer PageReference property is ignored in the lookup, and the only lookup applied is to the children of the StartPage.
    I am just guessing based on the code, it would be great if you take a look to see if that guessing makes sense. 🙂

    Second:

    Every page load several log entries are added to the log, they look like the following:

    2014-03-31 16:11:39,204 [81] INFO EPiServer.Framework.Initialization.InitializationEngine: InitializeHttpEvents successful for Initialize on class EPiServer.Initialization.LocalizationInitialization, EPiServer, Version=7.0.586.16, Culture=neutral, PublicKeyToken=8fe83dea738b45b7

    Do you know if this is expected?

    Third:

    I noticed that the package was downgraded to 4.0, just want to confirm that the downgrade was to allow EPiServer devs to use the package on .NET 4.0 solutions and not due to some sort of incompatibility.

    Once again, thank you for helping out on creating this useful library. So far the editors are happy with the ease of use. Just this little item for NLB that got in the way.

    Best Regards,
    – Wacdany

    Like

    1. First: I have looked into it and I cannot reproduce this behavior. I have moved my container outside of the root startpage, and it still is found. This is what I have on my startpage: “public virtual PageReference TranslationContainer { get; set; }”.
      Second: It looks like standard EPiServer logging. If the provider was initialized multiple times you would get different log entries starting with [Localization]
      Third: The downgrade was indeed for for the reasons you said 🙂

      Does it work with a load balancer now btw?

      Like

  5. Hello Jeroen,

    Thank you so much for your support,

    Second and Third: Ok perfect.

    First: Are you using the nuget dll or the one that you passed me in dropbox? By using the one in dropbox I keep getting the log message:
    [Localization] No translation container specified.

    My start page also has the following property pointing to a translation container: “public virtual PageReference TranslationContainer { get; set; }”

    Cheers,

    Like

  6. Hello Jeroen,

    Great, will test the updated dll this week and keep you posted. If everything goes correctly I should be able to let you know if the initial fix (for multiple-server environments) works as expected.

    Thanks!
    – Wac

    Like

  7. Hello Jeroen,

    I just managed to incorporate the new dll and the issue “[Localization] No translation container specified.” seems to be gone.
    I will keep you updated once this is tested in the multiple-server environment.

    Thanks and Regards,

    Like

  8. Hello Jeroen,

    I wanted to give you an update that we are experiencing one exception on the Localization provider.

    {code}
    at EPiServer.Framework.Localization.ProviderBasedLocalizationService.ProviderList_CollectionChanged(Object sender, NotifyCollectionChangedEventArgs e)
    at System.Collections.ObjectModel.ObservableCollection`1.OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    at EPiServer.Libraries.Localization.TranslationProviderInitialization.LoadProvider() in d:\Documents\GitHub\EPiServer.Libraries\EPiServer.Libraries.Localization\TranslationProviderInitialization.cs:line 322
    at EPiServer.Libraries.Localization.TranslationProviderInitialization.ReloadProvider() in d:\Documents\GitHub\EPiServer.Libraries\EPiServer.Libraries.Localization\TranslationProviderInitialization.cs:line 335
    at EPiServer.Libraries.Localization.TranslationProviderInitialization.RemoveFromCacheEventRaised(Object sender, EventNotificationEventArgs e) in d:\Documents\GitHub\EPiServer.Libraries\EPiServer.Libraries.Localization\TranslationProviderInitialization.cs:line 295
    at EPiServer.Events.EventNotificationHandler.Invoke(Object sender, EventNotificationEventArgs e)
    at EPiServer.Events.Clients.Event.Raise(Guid raiserId, Object param, EventRaiseOption raiseOption)
    at EPiServer.Events.Clients.Event.EPiServer.Events.Clients.IEventHandler.Raised(Guid raiserId, Guid eventId, Object param)
    at EPiServer.Events.Remote.RemoteEventsManager.ReceiveEvent(Guid raiserId, String siteId, Int32 sequenceNumber, Byte[] verificationData, Guid eventId, Object param)
    at EPiServer.Events.Remote.EventReplication.EPiServer.Events.ServiceModel.IEventReplication.RaiseEvent(Guid raiserId, String siteId, Int32 sequenceNumber, Byte[] verificationData, Guid eventId, Object param)
    at SyncInvokeRaiseEvent(Object , Object[] , Object[] )
    at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
    at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
    at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)
    at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)
    at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)
    {code}

    Please let me know if there is anything I can do to help solving this problem (this is already in the live website)

    Thank you so much in advance,
    – Wacdany

    Like

  9. Hello Jeroen,

    Sorry for the multiple comments, please find below the error message:

    Error Message:
    System.InvalidOperationException: There are already a provider with the same name in the list Translations. Rename your provider, or remove the old first

    Stacktrace:
    at EPiServer.Framework.Localization.ProviderBasedLocalizationService.ProviderList_CollectionChanged(Object sender, NotifyCollectionChangedEventArgs e)
    at System.Collections.ObjectModel.ObservableCollection`1.OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    at EPiServer.Libraries.Localization.TranslationProviderInitialization.LoadProvider() in d:\Documents\GitHub\EPiServer.Libraries\EPiServer.Libraries.Localization\TranslationProviderInitialization.cs:line 322
    at EPiServer.Libraries.Localization.TranslationProviderInitialization.ReloadProvider() in d:\Documents\GitHub\EPiServer.Libraries\EPiServer.Libraries.Localization\TranslationProviderInitialization.cs:line 335
    at EPiServer.Libraries.Localization.TranslationProviderInitialization.RemoveFromCacheEventRaised(Object sender, EventNotificationEventArgs e) in d:\Documents\GitHub\EPiServer.Libraries\EPiServer.Libraries.Localization\TranslationProviderInitialization.cs:line 295
    at EPiServer.Events.EventNotificationHandler.Invoke(Object sender, EventNotificationEventArgs e)
    at EPiServer.Events.Clients.Event.Raise(Guid raiserId, Object param, EventRaiseOption raiseOption)
    at EPiServer.Events.Clients.Event.EPiServer.Events.Clients.IEventHandler.Raised(Guid raiserId, Guid eventId, Object param)
    at EPiServer.Events.Remote.RemoteEventsManager.ReceiveEvent(Guid raiserId, String siteId, Int32 sequenceNumber, Byte[] verificationData, Guid eventId, Object param)
    at EPiServer.Events.Remote.EventReplication.EPiServer.Events.ServiceModel.IEventReplication.RaiseEvent(Guid raiserId, String siteId, Int32 sequenceNumber, Byte[] verificationData, Guid eventId, Object param)
    at SyncInvokeRaiseEvent(Object , Object[] , Object[] )
    at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
    at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
    at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)
    at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)
    at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)

    Cheers,

    Like

    1. Hi, sorry to hear about the problems. I should get a load balanced test environment one day.
      Anyway, I made some changes (I added a listener to a custom event, instead of the cache event, maybe to many cache update events were fired for the reloading of the provider) and added some extra checks. As always, the unit tests kept working, but I cannot test it on a load balanced environment. Maybe I can figure out how to fake that in a unit test. File is located here: https://www.dropbox.com/s/7wg7pcxsa19pfta/EPiServer.Libraries.Localization.1.0.0.3.zip

      Like

  10. Hello Jeroen,

    Thanks for your help! This will be a long message so I am dividing it in two.

    How to replicate the behavior:

    I can’t figure how to fake the load balanced environment in a unit test, since the only thing we need to troubleshoot this scenario is the Remote Events being triggered I did the following in order to “simulate” the environment:
    # Lets assume we start with a working IIS site named http://test.local This site will function as a internet facing server that listens to remote events and invalidates the cache as expected.
    1. Duplicate the site file structure (Copy-paste to a new folder)
    2. Create a new IIS site (http://test.admin.local pointing to the new folder)
    This new site will function as the admin server that publishes pages and triggers the remote events.
    3. Make sure the configuration for the remote events is as expected.
    For test.local:

    For test.admin.local:

    I added the following lines to my global.asax and enabled the debugging in Visual Studio for test.local:

    EPiServer.Events.Clients.Event removeFromCacheEvent = EPiServer.Events.Clients.Event.Get(CacheManager.RemoveFromCacheEventId);
    removeFromCacheEvent.Raised += this.RemoveFromCacheEventRaised;

    The event is raised successfully with a RaiserId different than CacheManager.LocalCacheManagerRaiserId.

    Best Regards,

    Like

  11. Hello Jeroen,

    This is the continuation of the previous message.
    Which fixes/improvements we can make:

    After looking at the code and noticing that the exception is occurring in the line:
    ” this.LocalizationService.Providers.Add(localizationProvider);”

    I was wondering which changes we can make to prevent the behavior and came out with a few ideas, in order of impact:

    1. Making the ReloadProvider method transactional: Adding a threadsafe lock to the method EPiServer.Libraries.Localization.TranslationProviderInitialization.ReloadProvider would help, so that the two lines initialized = this.UnLoadProvider(); and initialized = this.LoadProvider(); are treated as a single operation.

    2. Making variable “initialized” threadsafe: I noticed that the initialized variable is static, hence wrapping it with a initializedSync object might be the way to go to prevent multiple initializations.

    3. Checking if reloading of the provider is required: When debugging I noticed that the Id of the page is provided in the remote event in the property “Param” the value was: EP:ContentVersion:956 which happened to match the PageID of the published page. The following steps would prevent unnecessary provider reloads due to remote events:
    * When event is raised. Try to parse that value and get the referred ContentReference.
    * If parse is successful, check if the content reference corresponds (or inherits from) to a page of a relevant type [CategorieTranslationItem, CategoryTranslationContainer, TranslationContainer.cs , TranslationItem.cs]
    * If not, then there is no need to reload the provider.
    * If yes, then reload the provider.

    Please let me know if the changes stated above make sense from your standpoint, and be sure that we are more than glad to help for the implementation.

    Best Regards,
    – Wacdany

    Like

    1. Hi. Stupid of me, I did a simulation like that for a project. Thanks for reminding me 🙂
      Anyway, Did you test that last version I put on DropBox, or the last code version on GitHub? As your third point is exactly what I changed in the last version I put on DropBox. And I have not been able to reproduce your issue.
      Putting a lock could give some extra stability, but am not sure if it’s necessary with the custom event.

      Like

  12. Hello Jeroen,

    Haha, I was actually explaining to someone else the issue and then the idea came to mind. The code updates are good stuff, thanks 🙂

    I haven’t tried the latest code, as I have to follow the classic: reproduce-upgrade-test process, I was checking out the code on Github and noticed that the RaiserId GUID is hardcoded:

    private static readonly Guid RaiserId = new Guid(“cb4e20de-5dd3-48cd-b72a-0ecc3ce08cee”);

    Am I correct to assume that this would cause the TranslationsUpdatedEventRaised method to always fall into the following condition:

    if (e.RaiserId == RaiserId)
    {
    return;
    }

    I understand why the EventId has to be harcoded, but shouldn’t the RaiserId be generated per site by using Guid.NewGuid()?

    Once again, thanks a lot for your prompt help.

    Regarding your comment: “Putting a lock could give some extra stability, but am not sure if it’s necessary with the custom event.”
    After the testing, reproducing the issue consistently and upgrading we will figure out if it’s needed. For now let’s move on as is.

    Cheers,

    Like

  13. Hello Jeroen,

    I was using the dll’s but figured out that the code from GitHub is way better for debugging (my RedGate decompiler trial has expired :|)

    I did some setup for the testing as follows:
    1. Update the localization library
    2. Set up two IIS websites (edit server and webfront server) to simulate the behavior of two servers
    2.1 Configure the remote events section.
    3. Set up an extra Log4net appender:

    4. Setup a JMeter test to make HTTP-Requests to the “webfront” website. This is a simple test composed of:
    4.1 A Threadgroup with 50 users
    4.2 A HTTP Request Sampler
    4.3 A HTTP Response Code Assertion
    4.4 A listener “View Results Tree”

    The test conducted was:

    * Put the JMeter test to run.
    1. In the edit server, look for a translation node that has many children.(In my case 10 children translation pages)
    2. Move the last node to the top and accept the move operation when prompted.
    2.1 This triggers 10 InstanceChangedPage events.
    3. Check that there is an entry in the localization log:
    – 2014-06-23 12:44:05,270 [85] INFO EPiServer.Libraries.Localization.TranslationProviderInitialization: [Localization] Translations updated on other machine. Reloaded provider.
    4. Check that there are no errors in the JMeter listener.

    By doing so, we were able to find the following exception a few times:

    System.Web.HttpUnhandledException (0x80004005): Exception of type ‘System.Web.HttpUnhandledException’ was thrown. —> System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
    at System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource)
    at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
    at System.Collections.Generic.List`1.Enumerator.MoveNext()
    at EPiServer.Framework.Localization.ProviderBasedLocalizationService.LoadString(String[] normalizedKey, String originalKey, CultureInfo culture)
    at EPiServer.Framework.Localization.LocalizationService.TryGetStringByCulture(String originalKey, String[] normalizedKey, CultureInfo culture, String& localizedString)
    at EPiServer.Framework.Localization.LocalizationService.GetFallbackResourceValue(FallbackBehaviors fallbackBehavior, String resourceKey, CultureInfo culture, String[] normalizedKey)
    at EPiServer.Framework.Localization.LocalizationService.GetStringByCulture(String resourceKey, FallbackBehaviors fallbackBehavior, CultureInfo culture)
    at EPiServer.Framework.Localization.LocalizationService.GetStringByCulture(String resourceKey, CultureInfo culture)
    at XX.Shared.PropertySiteUserControlBase`1.Translate(String key) in XX\PropertySiteUserControlBase.cs:line 30

    This one seems like an episerver exception when updating the LocalizationProvider, however I wanted to let you know and see if you have any ideas on how to circumvent this?

    Thanks!

    Like

    1. mmmm, no I don’t think there is a way to circumvent this, it’s in the EPiServer base. So it might be bug in the base localization service. But I will do some digging.
      It’s quite an exceptional test scenario I think though. It doesn’t happen when updating only one translation?
      Does the FronEnd show this exception, or does it show the untranslated version?
      Anyway, maybe EPiServer should implement a concurrent collection, which is threadsafe and can be used as of .Net 4

      Like

  14. Hello Jeroen,

    It actually can be reproduced when updating only one translation, is just more unlikely for it to be replicated with so few concurrent visitors (50)
    The frontend shows the exception (500 error)

    I am afraid we reached the wall on this in terms of stability. A few ideas that come to my mind but are a bit too different to what we have been trying so far:
    * Make the realtime load-balancing configurable.
    * Create an scheduled job for the refreshing of the transactions.
    * Since this is unlikely to happen more than once (only while updating the provider). Adding Transient Fault Handling to the Translate methods.

    Let me do some more testing and come back to you with the results.

    Thank you so much for your help on this.

    Best Regards,

    Like

    1. Hi,
      A quick fix. Create a control adapter that catches the error like this:

      using System;
      using System.Linq;
      using System.Web.UI;
      using System.Web.UI.WebControls.Adapters;

      using EPiServer.Web.WebControls;

      using log4net;

      public class LocalizationControl : WebControlAdapter
      {
      private static readonly ILog Log = LogManager.GetLogger(typeof(LocalizationControl));

      protected new Translate Control
      {
      get
      {
      return base.Control as Translate;
      }
      }

      protected override void Render(HtmlTextWriter output)
      {
      try
      {
      base.Render(output);
      }
      catch (Exception exception)
      {
      Log.Error("[Localization] Error displaying translation.", exception);

      string displayText = string.Format(CultureInfo.InvariantCulture, " [Missing text '{0}'] ", this.Control.Text);
      ////string displayText = this.Control.Text.Split(new[] { '/' }).Reverse().FirstOrDefault() ?? string.Empty;
      output.Write(displayText);
      }
      }
      }

      Add something like the following to a .browser file

      adapter controlType=”EPiServer.Web.WebControls.Translate” adapterType=”EPiServer.Libraries.Localization.Web.LocalizationControl”

      Note that the adaptertype should contain the namespace you put the adapter in.

      In the mean time I will file a bug with EPiServer as I do not think the translate control should throw an error when there is an error retrieving a translation.
      I will also try to find out if there is a better solution.

      Like

    2. You might try to add this to the TranslationProvider. It overrides the base GetString method and ads a spin-wait construction. Although when testing I never got the exception you were getting, this might solve it. Though I think it should be fixed in the core. The spin-wait should not give a big cpu hit, but please check that.

      ///

      /// Gets a translated string from a language key.
      ///

      /// The unmodified key
      /// The normalized and split into an array
      /// The requested culture for the resource string
      /// A translated resource string
      public override string GetString(string originalKey, string[] normalizedKey, CultureInfo culture)
      {
      string translatedValue = null;

      // Wait for max a second max if the collection is being modified.
      SpinWait.SpinUntil(() => (this.TryGetString(originalKey, normalizedKey, culture, out translatedValue)), 1000);

      return translatedValue;
      }

      ///

      /// Tries to get a translated string from a language key.
      ///

      /// The unmodified key
      /// The normalized and split into an array
      /// The requested culture for the resource string
      /// A translated resource string.
      /// true if a translated value was retrieved, false otherwise.
      private bool TryGetString(string originalKey, string[] normalizedKey, CultureInfo culture, out string translatedValue)
      {
      translatedValue = null;

      // Prevent the spin-wait loop from consuming resources that a waiting thread may use, allowing the thread to yield if another thread is waiting.
      Thread.Sleep(0);

      // Get the translated value. Only return false on an InvalidOperationException, which can be caused if the collection is modified.
      try
      {
      translatedValue = base.GetString(originalKey, normalizedKey, culture);
      }
      catch (InvalidOperationException)
      {
      Logger.Debug("[Localization] Translation collection was modified. Entering wait.");
      return false;
      }
      catch (Exception exception)
      {
      Logger.Error("[Localization] Error getting translation.", exception);
      }

      return true;
      }

      Like

  15. Hey Jeroen,

    Hope everything is going well for you. First of all thanks for all the help on optimizing this provider.

    I just wanted to let you know that the previous scenario where moving pages caused the publishing of all the siblings has been fixed by EPiServer, so the likelihood of the issue (that you worked-around with the SpinWait has been reduced:

    http://world.episerver.com/Documentation/Release-Notes/ReleaseNote/?releaseNoteId=111661

    Cheers,
    – Wac

    PS: Saw that you updated the provider to 7.7.1 and above. which rocks!

    Like

Leave a comment