My colleague got a task to create a way of localizing file metadata. The requirements were to be as close to regular page translations that come out of the box. Unfortunately EPiServer does not support this kind of functionality yet so he needed to code it himself.
After a little research he found a blog post from 2014 with a hint on how to solve this problem. In that implementation the DataFactory LoadedContent, LoadedChildren and SavingContent events were used to store translations in DDS. My colleague tried to use the code and it looked like everything worked properly. The editor was able to edit alt text and show the content in different languages on frontend side.
But there were a few limitations regarding client requirements:
- After switching the language, the plugin would automatically load media from currently selected culture – there was no explicit Translate button.
- EPiServer fallbacks configuration was not used.
How the page localization works
He looked closer into how page localization works internally. Why the page can be localized while the media doesn’t. It should be possible, because they both inherit from ContentData class and implement the IContent interface right? 😉
However, it turned out that in fact there is a difference between PageData and MediaData. It’s all about EPiServer.Core.ILocalizable interface which is not implemented by MediaData.
The interface exposes two properties:
- ExistingLanguages – return all existing languages for content,
- MasterLanguage – returns the master language for content.
1 2 3 4 5 |
public interface ILocalizable : ILocale { IEnumerable ExistingLanguages { get; set; } CultureInfo MasterLanguage { get; set; } } |
He tried to simply add the ILocalizable interface to ImageFile media model.
Implementation
Implementing ILocalizable interface in ImageFile class was not difficult. Just added those two properties to the class :).
1 2 3 4 5 6 7 8 9 10 |
[ContentType(GUID = "0A89E464-56D4-449F-AEA8-2BF774AB8730")] [MediaDescriptor(ExtensionString = "jpg,jpeg,jpe,ico,gif,bmp,png")] public class ImageFile : ImageData, ILocalizable { public IEnumerable ExistingLanguages { get; set; } public CultureInfo MasterLanguage { get; set; } // other properties } |
The code compiled, but the media still didn’t worked. In translated versions the image was not available (404 status was returned). The problem was that the BinaryData property is stored in CultureSpecific way. The MediaData implements the IBinaryStorable interface which is the key here.
The IBinaryStorable interface is used by RawContentRetriever class to create content. In the CreateRawContent methods, the list of content properties is extended. The list of new properties depends on which interfaces are implemented by the content (like IVersionable, IResourceable, ICategorizable etc.). After IBinaryStorable is implemented, then BinaryData string property is added.
1 |
RawProperty metaDataProperty = MetaDataProperties.CreateRawMetaDataProperty(new MetaDataProperties.MetaData(0, "BinaryData", PropertyDataType.String, false, true)); |
The last parameter of MetaData class constructor is boolean isLanguageSpecific which is set to true. It means that for different languages media could store different binary content. Usually the editor don’t need to change the whole image, but for example just the alt text.
When clicking Translate on a media item, a new language branch is created. All culture specific properties are set to null. This is why this solution did not worked. The property with media URL was blank. In this case it should be treat as not localizable. So the only thing that have to be overridden is the behavior of BinaryData and Thumbnail properties. We need to ensure that the value is always loaded from Master Language.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public override Blob BinaryData { get { if (ContentReference.IsNullOrEmpty(this.ContentLink)) { return base.BinaryData; } if (this.Language.Name == this.MasterLanguage.Name) { return base.BinaryData; } var contentRepository = ServiceLocator.Current.GetInstance(); var content = contentRepository.Get(this.ContentLink.ToReferenceWithoutVersion(), new LanguageSelector(this.MasterLanguage.Name)); return content.BinaryData; } set { base.BinaryData = value; } } |
Now the edit mode for Media looks like for Pages and Blocks.
The All properties editing where non-localizable properties are in readonly state.
The full source code is below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
[ContentType(GUID = "0A89E464-56D4-449F-AEA8-2BF774AB8730")] [MediaDescriptor(ExtensionString = "jpg,jpeg,jpe,ico,gif,bmp,png")] public class ImageFile : ImageData, ILocalizable { public IEnumerable ExistingLanguages { get; set; } public CultureInfo MasterLanguage { get; set; } public virtual string Copyright { get; set; } [CultureSpecific(true)] public virtual string Title { get; set; } [CultureSpecific(true)] public virtual string AlternateText { get; set; } public override Blob BinaryData { get { if (ContentReference.IsNullOrEmpty(this.ContentLink)) { return base.BinaryData; } if (this.Language.Name == this.MasterLanguage.Name) { return base.BinaryData; } return this.LoadMasterContent().BinaryData; } set { base.BinaryData = value; } } [ImageDescriptor(Height = 48, Width = 48)] public override Blob Thumbnail { get { if (ContentReference.IsNullOrEmpty(this.ContentLink)) { return null; } if (this.Language.Name == this.MasterLanguage.Name) { return base.Thumbnail; } return this.LoadMasterContent().Thumbnail; } set { base.Thumbnail = value; } } } public static class LocalizableMediaDataExtensions { public static T LoadMasterContent<T>(this ImageFile imageFile) where T : IContent { var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>(); var content = contentRepository.Get<T>(imageFile.ContentLink.ToReferenceWithoutVersion(), new LanguageSelector(imageFile.MasterLanguage.Name)); return content; } } } |