LibrarySites.Banner

Fallback Series Post 4: Enforcing Language Version Presence, Customizations

In my previous post, I discussed the out-of-the-box functionality for enforcing language version presence in Alex Shyba's Partial Language Fallback Module

By enabling the enforceVersionPresence setting, content authors are able to dictate whether a page exists on a site in a particular language by adding or removing the language version for that item. 

There are a few helpful customizations in this vein that I'd like to discuss in this post.

First, if planning to NOT enforce language version presence on specific types of items, like Media, and you are planning to use chained languages (es-US falls back to en-US which falls back to en), then an update needs to be made within the Sitecore.SharedSource.PartialLanguageFallback project and compiled dll.  Within the FallbackLanguageManager.cs there is a method called ReadFallbackValue: it gets the fallback language version for a particular item language and returns the field value from that version.  The problem with that is if you don't create that middle language version in a chained scenario, then the check it makes against fallbackItem.Versions.Count, will prevent it from getting the fallback value from the final language in the chain, and will return a null.  It is really important that for this scenario it won't be concerned with the language version existing.  Therefore, you replace:

return fallbackItem != null && fallbackItem.Versions.Count > 0 ? fallbackItem[field.ID] : null;

With

return fallbackItem != null ? fallbackItem[field.ID] : null;

Next, when enforceVersionPresence is not turned on, if a language version does not exist for an item and you load it on the front-end in that language, it will load, but it could be blank, if that language does not fall back or falls back to a language that also does not exist for that item.  I felt that this isn't an ideal user experience.  Why would you ever want a page to load up blank?  And so although enforceVersionPresence may not be on, if fallback is enabled, then I would still want some sort of enforcement to prevent this blank content.

I also felt that the need to enter template GUIDs to check against via the EnforceVersionPresenceTemplates configuration setting could be limiting.  I understand why it was necessary in Alex's code, because of where he is running that code.  It is run when any kind of item is retrieved and therefore is hit in ways beyond just serving up a page.  You don't want it running for many items that help make Sitecore work.  But if you hook into the process a little later down the line, the httpRequestBegin pipeline, then you are only dealing with content being served with an HTTP request.

You can use this instead of Alex's Enforce Version logic (by commenting out the GetItemCommand configurations in the Sitecore.SharedSource.PartialLanguageFallback.config), or in addition to it (demo uses both).

We had noticed that the GetItemCommand custom updates that come with Partial Language Fallback potentially caused some issues with TDS and programmatic inserts to Sitecore, so you may need to comment it out anyway based on whether you run into similar issues.

I added a custom processor to the Sitecore.SharedSource.PartialLanguageFallback.config which is patched into the httpRequestBegin pipeline, after the ItemResolver:

<processor patch:after="*[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" type="Verndale.SharedSource.SitecoreProcessors.ItemLanguageValidation, Verndale.SharedSource" />

Within the ItemLanguageValidation Process method, if the Sitecore Context item is not null, and it is not the current site's Page Not Found page, and if my custom method LanguageHelper.HasContextLanguage comes back false, then it will redirect the user to the 404 page setup for the current site.   The GUID of this page is set within the site node in a pageNotFoundGUID setting.

IMPORTANT: Your 404 page should be setup for every language of your site.  That language version NEEDS to exist so that it can redirect to it in that language.  Otherwise you'll get a 404 error for your 404 page.

The HasContextLanguage method has the guts of the logic for determining whether enforceVersionPresence is explicitly set, or should still be enforced because fallback is enabled and it won't fallback to anything.  The logic within it does the following: (note, by returning true, it is telling the ItemLanguageValidation process that the item exists and should be served up.

  • If Sitecore.Context.Site is null or the enableFallback property doesn't exist, none of the rest should matter, return true
  • If the item has a version in the current Context language, return true
  • If the item does not have a version in the current Context Language and enforceVersionPresence is turned on and the item's template or base template GUID is in the Fallback.EnforceVersionPresenceTemplates, return false (404)
    • At this step, you could ignore that setting completely and just enforce for everything, if you did not want to worry about adding specific template GUIDs.
  • If enableFallback is not enabled for the site and there isn't a version in the current Context Language, then it doesn't matter if enforceVersionPresence is on or not, we don't want a blank version, return false (404)
  • If enableFallback is turned on and there is no Context Language version:
    • If the current Context Language doesn't fallback, return false (404)
    • If the current Context Language DOES fallback, then recursively check the fallback language for a language version
      • If it never finds a language version, return false (404), otherwise return true

 

Not only will I reference this HasContextLanguage method from the httpRequestBegin pipeline, but I also wrap calls to Children and GetChildren with a method, RemoveItemsWithoutLanguageVersion, which will call the HasContextLanguage method to remove anything that shouldn't be displayed. 

Recently I found another scenario that falls outside of just enforcing versioning on language. 

My client had multiple regional sites, with 90% of the content and site structure exactly the same.  And there was A LOT of content.  This made me rule out different site nodes and cloning.  Most of the time language versioning with enforceVersionPresence turned on would do the job. 

But users could select one of several dozen countries to view the site through, and then within each country at least one or two different language options.  Assuming that in most of those cases it truly was a multilingual translation difference (no differences in content or structure by region), there would be no need to add an English, Spanish, and German language version for EACH country; that would be totally unnecessary overhead.

Instead, we could have English, Spanish and German for each major region, and create Country items in Sitecore that map to the same exact languages.  When the user selects a Country and Language, we save the Country to session and then the language is the same Language version regardless of which country they selected in that region.

BUT there is always an exception.  What if there is a page that should only be visible in one of those countries?  Now what?  Do we have to add a language for that country, and add that language version to EVERY item in the content tree and media library? (If we decided yes, there is a tool to do that!  I will discuss that in a later blog post).  This is a one-off scenario.  My solution was to add a treelist field to a base page template named 'Limit To Countries' which lists all Country items in the system.  If this field has no value, the logic ignores it completely.  If one or more Country items are selected, then the code uses the value in Session to determine if the requested page is a match on country and if not, will return a 404 page.

I could not hook into this via the httpRequestBegin pipeline, since the Session was not available and that is where the current user's Country is saved.  Instead, I added it to the web.config, in two locations:

In <system.webServer><modules>

<add type="Verndale.SharedSource.SitecoreProcessors.RegionValidationModule, Verndale.SharedSource" name="RegionValidationModule" />

And in <system.web><httpModules>

<add type="Verndale.SharedSource.SitecoreProcessors.RegionValidationModule, Verndale.SharedSource" name="RegionValidationModule" />

This references a class called RegionValidationModule.  It implements IHttpModule and the RequestHandler starts after acquiring state.  This is almost the same exact logic as the ItemLanguageValidation processor:  if the Sitecore Context item is not null, and it is not the current site's Page Not Found page, and if my custom method LanguageHelper.IsValidForCountry comes back false, then it will redirect the user to the 404 page for the current site.  

IsValidForCountry will first make sure it can get at the Session variable for Country (making sure session isn't null, etc).  It then checks the item's 'Limit to Countries' field.  If no countries are selected, then it will return true.  We only want to restrict if at least one country is selected.  If there are any countries selected, it checks if the current Session country is one of them and if it is NOT, then it will return false, which will result in a 404 page.  The RemoveItemsWithoutLanguageVersion that I mentioned above will also use this method to make sure whatever we wrap with it won't include those items not valid for the country.

So to sum up, there is a lot to consider when enforcing language version presence via the Partial Language Fallback Module.  The above customizations could help with more complex scenarios.  Ultimately, it all comes down to the strategy needed for the site you are implementing.

Link to Resources:

https://github.com/Verndale-Corp/Sitecore-Fallback-Enforce-Version-Customizations

https://github.com/Verndale-Corp/Sitecore-Fallback-FullDemo