Language fallback for media library item in sitecore - sitecore

I have a number of PDFs in English language. I have web pages in English and German lang.
If in a German Page I want to display PDF of English lang, it is not possible as that German version PDF is not available, so I tried to do fallback for media library item, even then no help.
So can someone please tell me any alternative for this.
NOTE: I don't want to upload english document in German version, as there are other languages available and customers cannot upload those many times in all lang.
I need to upload a document in Only English but display in all other Languages irrespective of that document is there in that lang or not.
It's ok even if I need to make changes through code.
Thanks in advance

Which template are you using to upload your PDF? If you are using /sitecore/templates/System/Media/Unversioned/Pdf then this inherits from /sitecore/templates/System/Media/Unversioned/File and the blob field for this is marked as shared anyway:
Shared fields are shared across language versions, so if you upload an English PDF and link to that same media item from a German item then it will link to the original English PDF.
In Sitecore, when adding a field to a template, there's a checkbox called "shared". What's it for?

There are a couple of ways to do this. You can do it in the code for your rendering, or you can use the language fallback module from the Sitecore marketplace.
To do it in code you would need to create a new MediaProvider. Create a class that inherits from Sitecore.Resources.Media.MediaProvider and override the protected virtual MediaData GetMediaData(MediaUri mediaUri) method.
This method gets the sitecore item for the context language or the language in the Uri. So you can implement the fall back here:
public class MediaProviderWithFallback : Sitecore.Resources.Media.MediaProvider
{
protected override Sitecore.Resources.Media.MediaData GetMediaData(Sitecore.Resources.Media.MediaUri mediaUri)
{
Assert.ArgumentNotNull((object)mediaUri, "mediaUri");
Database database = mediaUri.Database;
if (database == null)
{
return null;
}
string mediaPath = mediaUri.MediaPath;
if (string.IsNullOrEmpty(mediaPath))
{
return null;
}
Language language = mediaUri.Language;
if (language == null)
{
language = Context.Language;
}
Sitecore.Data.Version version = mediaUri.Version;
if (version == null)
{
version = Sitecore.Data.Version.Latest;
}
Sitecore.Data.Items.Item mediaItem = database.GetItem(mediaPath, language, version);
if (mediaItem == null)
{
return (MediaData)null;
}
// Check for language fallback
if (mediaItem.Versions.Count == 0)
{
// Workout your language fallback here from config or sitecore settings etc...
language = Language.Parse("en");
// Try and get the media item in the fallback language
mediaItem = database.GetItem(mediaPath, language, version);
if (mediaItem == null)
{
return null;
}
}
return MediaManager.Config.ConstructMediaDataInstance(mediaItem);
}
}
Please note - this is untested code. You should store your fallback in config or modify the language template in sitecore.
Once you have that class you will need to update your web.config to use your provider over Sitecores. So find this section in the web.config and change the type to be your class and assembley:
<!-- MEDIA PATH -->
<mediaPath defaultProvider="default">
<providers>
<clear />
<add name="default" type="Sitecore.Resources.Media.MediaPathProvider, Sitecore.Kernel" />
</providers>
</mediaPath>

It's best practice to try to identify whether media will need to be versioned ahead of time. If you know media is going to need to be versioned based on language, you should make sure to update in your web.config the following attribute:
<!--By default, Media items are not versionable and the below setting is set to false in the web.config.
If you upload an image in one language, it will persist across all language versions.
If you change this to true, then versioning will apply and you would have to set the media item into all language versions,
or enable fallback, but if enforce version presence is turned on and media template guids are included in EnforceVersionPresenceTemplates,
then you'll have to make sure all language versions at least exist-->
<setting name="Media.UploadAsVersionableByDefault">
<patch:attribute name="value">true</patch:attribute>
</setting>
Alex Shyba's Partial Language Fallback module will successfully work with this. I would recommend making sure not to enforce version presence on any media templates (don't want to force admins to have to create blank language versions). Then they can create english versions and then only create a language version when they need to override it.
You will need, in the case of using partial language fallback, to make sure the enable fallback checkboxes are checked on the media versionable template fields.
I also recommend updating the media provider so that it embeds language into the media url so that caching doesn't come into play. EG: if you create a pdf named Directions.pdf and it loads at www.site.com/media/Directions.pdf, when you switch between languages, it very well could cache it. So you would want to update the media provider to encode the media url with the context language.
You can see a demo here:
https://github.com/Verndale-Corp/Sitecore-Fallback-FullDemo
public class CustomMediaProvider : MediaProvider
{
public override string GetMediaUrl(MediaItem item, MediaUrlOptions options)
{
Assert.ArgumentNotNull((object)item, "item");
Assert.ArgumentNotNull((object)options, "options");
string result = base.GetMediaUrl(item, options);
// Added by Verndale, check if language should be embedded
UrlOptions urlOptions = UrlOptions.DefaultOptions;
urlOptions = LanguageHelper.CheckOverrideLanguageEmbedding(urlOptions);
if (urlOptions.LanguageEmbedding == LanguageEmbedding.Always && options.UseItemPath)
{
result = "/" + Sitecore.Context.Language.Name.ToLowerInvariant() + Sitecore.StringUtil.EnsurePrefix('/', result);
}
return result;
}
public static UrlOptions CheckOverrideLanguageEmbedding(UrlOptions urlOptions)
{
var thisSite = Sitecore.Context.Site;
if (urlOptions.Site != null)
thisSite = urlOptions.Site;
if (!String.IsNullOrEmpty(thisSite.SiteInfo.Properties["languageEmbedding"]))
{
if (thisSite.SiteInfo.Properties["languageEmbedding"].ToLower() == "never")
urlOptions.LanguageEmbedding = LanguageEmbedding.Never;
else if (thisSite.SiteInfo.Properties["languageEmbedding"].ToLower() == "always")
urlOptions.LanguageEmbedding = LanguageEmbedding.Always;
else if (thisSite.SiteInfo.Properties["languageEmbedding"].ToLower() == "asneeded")
urlOptions.LanguageEmbedding = LanguageEmbedding.AsNeeded;
}
return urlOptions;
}
}

Related

Sitecore display name url does not work without language code

On this Sitecore website (6.5.0 rev. 120706), I have this sitecore item called XYZ. So I have http://example.com/XYZ/.
I've added french localization, so using display names I now have:
http://example.com/XYZ-en/
http://example.com/XYZ-fr/
The english version works well, but the french does not and resolves to 404 unless I go to the english first, click on my language switcher button first. When I click on it, I'm redirected to http://example.com/fr-CA/XYZ-fr/, which works. From then on, the english url stops working, the french one works. As I switch languages like that, I always only have one of the two that work.
That button runs this code:
protected void LanguageLinkClick(object sender, EventArgs e)
{
var lang = (Sitecore.Context.Language.Name == "en") ? "fr-ca" : "en";
Tools.RedirectToLanguage(lang, Response);
}
That Tools function runs the following code:
public static void RedirectToLanguage(string pStrLangToSet, HttpResponse pResponse)
{
if (!string.IsNullOrEmpty(pStrLangToSet))
{
var newLang = Language.Parse(pStrLangToSet);
if (newLang != null)
{
Sitecore.Context.SetLanguage(newLang, true);
var itm = Sitecore.Context.Item;
if (Sitecore.Context.Item != null)
{
var itemInLang = Sitecore.Context.Database.Items[itm.ID, newLang];
if (itemInLang != null)
{
pResponse.Redirect(BuildUrl(itemInLang));
}
}
}
}
}
This is somewhat old code that is in this old project.
Is there anything I should look for that would intercept default display name behavior? Or isthis behavior with display names something that's not managed out of the box?
Thanks for any help!
This is the expected behaviour. This article from John West describes the steps involved in the language resolving process and details of the ItemResolver process can be found here.
What I assume you have set in the LinkProvider for your site is useDisplayName=true and maybe languageEmbedding=false, something like the following:
<add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel"
alwaysIncludeServerUrl="false" addAspxExtension="true" encodeNames="true"
languageLocation="filePath" lowercaseUrls="false" shortenUrls="true"
languageEmbedding="never" useDisplayName="true" />
This tells the LinkManager to build URLs using the display name of the Item, hence you have multilingual URLs. Your RedirectToLanguage method switches the context language, from EN to FR-CA and vice versa, which puts Sitecore into that specific language mode for the user.
The following pipeline in <httpRequestBegin> tries to resolve an Item from your requested URL:
<processor type="Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel" />
And one of the attempts in that method is to ResolveUsingDisplayName(args). So it will be trying to resolve an Item with an EN display name, but the Context Language is set to FR-CA so in fact it will never find the item, hence the 404.
You have 2 options here:
Set languageEmbedding="always" in your LinkProvider, which mean your URLs will be formatted like "/en/Nice-Product-With-Lots-Of-Options" and "/fr-ca/Mon-Produit-Avec-Plusieurs-Options".
Add a custom pipeline processor after the default ItemResolver which tries to resolve using the alternate language, and if it finds a match sets the Context Item and switches language.
The following will call the default Sitecore ItemResolver after you switch to the alternate language and then attempt to resolve, which will try to find the Item using display name:
public class AlternateLanguageItemResolver : HttpRequestProcessor
{
public override void Process(HttpRequestArgs args)
{
Assert.ArgumentNotNull(args, "args");
if (Context.Item != null || Context.Database == null || args.Url.ItemPath.Length == 0)
return;
var origLanguage = Sitecore.Context.Language;
Sitecore.Context.Language = AltLanguage;
// try to find it using default ItemResolver with alternate language
var itemResolver = new Sitecore.Pipelines.HttpRequest.ItemResolver();
itemResolver.Process(args);
if (Context.Item == null)
{
// well we didn't find it, so switch the context back so everyting can continue as normal
Sitecore.Context.Language = origLanguage;
return;
}
// We found the Item! Switch the User Language for future requests
Sitecore.Context.SetLanguage(AltLanguage, true);
}
private Language _altLanguage = null;
private Language AltLanguage
{
get
{
if (_altLanguage == null)
{
var altLang = (Sitecore.Context.Language.Name == "en") ? "fr-ca" : "en";
_altLanguage = Language.Parse(altLang);
}
return _altLanguage;
}
}
}
And patch it in after the default ItemResolver:
<processor type="Sitecore.Sample.AlternateLanguageItemResolver, Sitecore.Sample"
patch:after="processor[#type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"/>
This is an untested prototype, check that subsequent requests are returned in the correct switched language. Otherwise redirect back to itself like you in your original code.
So if I understand correctly you'd like these urls to resolve the language for you:
http://example.com/XYZ-en/
http://example.com/XYZ-fr/
The LanguageResolver will resolve the language in this order:
1. language can be distilled from the url (cannot be done with above urls)
2. a language cookie is available for this site (sitename#lang)
3. fall back to default configured language
When you switch, using your switcher, the url paths start with the language code, then the resolver is able to resolve the language by url and persists the language in the language cookie. This is why you experience this behavior of one working language at a time.
The best you can do is always use language in urls (linkprovider configuration), otherwise you'd have to hook in the languageresolver and do some funky check on displayname for each language, but that will probably get very expensive.

Enrolling Anonymous Users in Engagement Plans

I know that it is possible to enroll users in an engagement plan from with Sitecore by adding them to a specific state in the plan when they visit a campaign URL, adding them when they submit a Web Forms for Marketers Form, and manually adding them in the Supervisor interface.
Additionally, I know that you can use the API to add a user as described here:
http://briancaos.wordpress.com/2013/06/03/programming-for-sitecore-dms-engagement-plans/
However, that method requires a username.
I would like to enroll anonymous users in an engagement plan when they visit any page represented by a particular template in Sitecore (ie, page from the Product template). Is this possible using the API?
To expand on my above comment, and to supplement your own answer, here's a processor that you could add to the after the ItemResolver in the httpRequestBegin pipeline that would achieve the desired result. It is a very basic version that you could embellish as you see fit
class CampaignRedirect
{
public void Process(HttpRequestArgs args)
{
var request = HttpContext.Current.Request;
// must not already have the querystring in the URL
if(request.QueryString["sc_camp"] != null &&
request.QueryString["sc_camp"] != "XXXXXXXX")
return;
// must have a context item
if(Sitecore.Context.Item == null)
return;
var item = Sitecore.Context.Item;
// must be the right template
if(item.TemplateID.ToString() != "{XXXXXXXXX-XXXX-XXXXXX}")
return;
var basicUrl = LinkManager.GetItemUrl(item);
var response = HttpContext.Current.Response;
response.Redirect(basicUrl + "?sc_camp=XXXXXXX");
}
}
If you're not familiar with adding processors, take a look here.
Per Sitecore support, this is not currently possible. However, I was able to achieve what I wanted by adding a jQuery AJAX call to the campaign URL to the sublayout used by the page type in question. Naturally this only works for clients with JS enabled, but for my purposes, that is not an issue.
<script type="text/javascript">$(function() { $.get('/?sc_camp=[campaignid]'); });</script>
Edited 2014-05-19
I found a way to do this via the Sitecore API. This is rough and needs to check for null values, exceptions, etc., but it does work:
string cookieVal = Request.Cookies["SC_ANALYTICS_GLOBAL_COOKIE"].Value;
List<Guid> guids = new List<Guid>() {
new Guid(cookieVal)
};
Guid automationStateId = new Guid("{24963AE9-1C8C-4E18-8EEE-01BC249D1F1B}");
Guid automationId = Sitecore.Context.Database.GetItem(new Sitecore.Data.ID(automationStateId)).ParentID.ToGuid();
Sitecore.Analytics.Automation.Data.AutomationManager.Provider.CreateAutomationStatesFromBulk(guids, automationId, automationStateId);

Implementing Sitecore Multisite Robots.txt files

How to implement to have different robots.txt files for each website hosting on the same Sitecore solution. I want to read dinamically robots.txt from sitecore items.
you need to follow next steps:
1) Create and implement your custom generic (.ashx) handler.
2) In the web.config file add the following line to the section
3) Navigate to the section and add here
4) On home item you will have "Robots" field (memo, or multi line field, not richText field)
Your custom generic handler will look like :
public class Robots : IHttpHandler
{
public virtual void ProcessRequest(HttpContext context)
{
private string defaultRobots = "your default robots.txt content ";
string robotsTxt = defaultRobots;
if ((Sitecore.Context.Site == null) || (Sitecore.Context.Database == null))
{
robotsTxt = defaultRobots;
}
Item itmHomeNode = Sitecore.Context.Database.GetItem(Sitecore.Context.Site.StartPath);
if (itmHomeNode != null)
{
if ((itmHomeNode.Fields["Robots"] != null) && (itmHomeNode.Fields["Robots"].Value != ""))
{
robotsTxt = itmHomeNode.Fields["Robots"].Value;
}
}
context.Response.ContentType = "text/plain";
context.Response.Write(robotsTxt);
}
We had similar problems especially in the multi site environment, so we used the handlers for implementing robots.txt
Create a new class inheriting from IHTTPHandler and implement the logic within the process method. Write the XML ouput to the context object.
context.Response.ContentType = "text/plain";
context.Response.Output.Write({XML DATA});
Add the custom handler and trigger.
<handler trigger="~/Handlers/" handler="robots.txt"/>
<add name="{Name}" path="robots.txt" verb="*" type="{Assembly Name and Type}" />
It seems that if you want to access Sitecore Context, and any items, you need to wait untill this stuff is resolved. The aboce method will always give you a null in the Site definition, as this isnt resolved when the filehandler kicks in.
It seems that to get the Sitecore.Context, you should implement a HttpRequestProcessor in Sitecore, that renderes the robots.txt, example on this website:
http://darjimaulik.wordpress.com/2013/03/06/how-to-create-handler-in-sitecore/
You can refer to this blog post for step-by-step explanation on how to do it with a custom HttpRequestProcessor and a custom robots settings template : http://nsgocev.wordpress.com/2014/07/30/handling-sitecore-multi-site-instance-robots-txt/

Can Glass.Mapper V3 support language fallback (field-level and item-level)?

We just updated our project to use Glass.Mapper V3. We LOVE it. But we've encountered an issue. It doesn't seem to respect language fallback.
We have our site set up so that if a user picks a non-default language, they will see the item of that language if it exists. If not, they will see the default ("fallback") language version. We have also set this up at the field level, so that if there is a non-default version of an item but not all the fields are changed, any unchanged fields will fall back to the default language version's value for that field.
Is there anything we can do to enable Glass to use language fallback?
I am updating this with a bit of background on why we do the check. If you ask for a Sitecore item that doesn't exist you get a null value, so that is simple to handle. However if you ask for a Sitecore item that doesn't exist in that particular language returns an item with no versions. This means we have to do this check because otherwise Glass would end up returning empty class which I don't think makes much sense.
This answer will get a little experimental.
First in the the Spherical.cs file you need to disable the check:
protected void Application_BeginRequest()
{
Sitecore.Context.Items["Disable"] = new VersionCountDisabler();
}
We can then move the check to later on to the Object Construction pipeline. First create a task:
public class FallbackCheckTask : IObjectConstructionTask
{
public void Execute(ObjectConstructionArgs args)
{
if (args.Result == null)
{
var scContext = args.AbstractTypeCreationContext as SitecoreTypeCreationContext;
if (scContext.Item == null)
{
args.AbortPipeline();
return;
}
//this checks to see if the item was created by the fallback module
if (scContext.Item is Sitecore.Data.Managers.StubItem)
{
return;
}
// we could be trying to convert rendering parameters to a glass model, and if so, just return.
if (String.Compare(scContext.Item.Paths.FullPath, "[orphan]/renderingParameters", true) == 0)
{
return;
}
if (scContext.Item.Versions.Count == 0)
{
args.AbortPipeline();
return;
}
}
}
}
Then finally register this task in the GlassMapperScCustom class:
public static void CastleConfig(IWindsorContainer container){
var config = new Config();
container.Register(
Component.For<IObjectConstructionTask>().ImplementedBy<FallbackCheckTask>().LifestyleTransient()
);
container.Install(new SitecoreInstaller(config));
}
I haven't tested this but it should in theory work <- disclaimer ;-)
There are few potential issues with provided solution when sitecore 7 (7.2) + IoC + solr + mvc is used.
When using IoC ex Winsdor please make sure that your Global.asax looks like this one <%# Application Codebehind="Global.asax.cs" Inherits="Sitecore.ContentSearch.SolrProvider.CastleWindsorIntegration.WindsorApplication" Language="C#" %>. Once, by mistake this file has been changed to <%# Application Codebehind="Global.asax.cs" Inherits="Merck.Manuals.Web.Global" Language="C#" %> and language fallback was not working. Also the errors we were getting wasn't descriptive as we thought that solr schema is incorrect.
Code Sitecore.Context.Items["Disable"] = new VersionCountDisabler(); can be added to PreprocessRequestProcessor and it works fine which is better solution that modifying global.asax.

Rename language after item is created

I'm using sitecore 6.5 with two languages installed, en (default) and fr-CA. There are items in the tree with content in both en and fr-CA.
The problem is that the French url has 'fr-CA' in it and we want that to be 'fr', for example:
http://website.com/fr/page.aspx instead of http://website.com/fr-CA/page.aspx
I tried renaming the language from 'fr-CA' to 'fr' and that fixed the url but the content still points to the old language 'fr-CA', so the item shows three languages: en, fr and fr-CA. It's not recognizing the name change.
Any suggestions are much appreciated.
Thanks,
Tarek
The problem is you have created fr-CA versions of your items which cannot be fixed by renaming the language .. you can now make a fr version but, like you are seeing, this means there are now 3 possible versions.
One suggestion is to leave the languages in Sitecore alone and alter how links are served and processed instead.
You would probably need to look at adding your own method into the httpRequestBegin pipeline in Sitecore. This would follow the LanguageResolver entry. You can then parse the RawUrl and set Sitecore.Context.Langauge' to French if the first element in it matched/fr/`.
Extremely quick & dirty example:
public class MyLanguageResolver : HttpRequestProcessor
{
public override void Process(HttpRequestArgs args)
{
string languageText = WebUtil.ExtractLanguageName(args.Context.Request.RawUrl);
if(languageText == "fr")
{
Sitecore.Context.Language = LanguageManager.GetLanguage("fr-CA");
}
}
}
You would probably also have to override the LinkProvider in the <linkManager> section of the web.config to format your URLs when they are resolved by Sitecore.
Another extremely quick & dirty example:
public class MyLinkProvider : LinkProvider
{
public override string GetItemUrl(Sitecore.Data.Items.Item item, UrlOptions options)
{
var url = base.GetItemUrl(item, options);
url = url.Replace("/fr-CA/", "/fr/");
return url;
}
}
Another way (slightly more long-winded as it will need to be executed via a script) is to copy the data from the fr-CA version to the fr version and then delete the fr-CA version of each item.
Rough helper method that encompasses what you're trying to do
private void CopyLanguage(ID id, Language sourceLanguage, Language destinationLanguage)
{
var master = Database.GetDatabase("master");
var sourceLanguageItem = master.GetItem(id, sourceLanguage);
var destinationLanguageItem = master.GetItem(id, destinationLanguage);
using (new SecurityDisabler())
{
destinationLanguageItem.Editing.BeginEdit();
//for each field in source, create in destination if it does not exist
foreach (Field sf in sourceLanguageItem.Fields)
{
if (sf.Name.Contains("_")) continue;
destinationLanguageItem.Fields[sf.Name].Value = sf.Value;
}
destinationLanguageItem.Editing.AcceptChanges();
////Remove the source language version
ItemManager.RemoveVersions(sourceLanguageItem,sourceLanguage, SecurityCheck.Disable);
}
}
Another way to update the languages on your content items is:
Export the fr-CA language to a .xml file (Using the Control Panel)
In the .xml file replace all and tags with the and
Rename fr-CA language in the master database to the fr
Import language from the .xml file
Run Clean Up Databases task (from the Control Panel)
Also you can create a sql script that will change fr-CA language with the fr for all records in the UnversionedFields and VersionedFields tables.
If you need any more information or examples please let me know. :)
I had a similar requirement to rename a language while retaining the content. I decided to migrate content from one language to another by using Unicorn:
1: Create a predicate telling Unicorn to track all of your content. In my case:
<include name="site content" database="master" path="/sitecore/content/mySite" />
Reserialize the content, writing it to disk as YML files
Using a tool that can perform a find & replace in multiple files at once, such as Notepad++, replace all instances of "Language: fr-CA" with "Language: fr" in your yml files.
Run a Unicorn Sync
You will find that all of your content is now associated with the "fr" language instead of "fr-CA".