Tales from the Evil Empire

Bertrand Le Roy's blog

News


Bertrand Le Roy

BoudinFatal's Gamercard

Tales from the Evil Empire - Blogged

Blogs I read

My other stuff

Archives

January 2011 - Posts

Building a favicon module for Orchard

(c) Bertrand Le Roy 2011I built a little module for Orchard tonight and it involves a couple of interesting magic tricks so I thought I'd share.

The module is replacing the default Orchard favicon with whatever you configure in site settings:

The default favicon The new favicon
The site settings for favicon

I haven't created any specific storage for the icons and instead decided to use a simple convention for the already existing Media module. What you can see in the site settings in the picture above is a list of suggestions that is just a list of the icons that are found in the favicon subfolder of the Media module. In order to do that, I added a reference to Orchard.Media in my module's csproj and the corresponding dependency in the module's manifest.

That done, I can get a dependency on IMediaService injected into my site settings part driver and get the files' URLs from the favicon subfolder:

faviconSuggestions = new List<string>(
    _mediaService.GetMediaFiles("favicon")
    .Select(f =>
_mediaService.GetPublicUrl("favicon/" + f.Name)));

The really tricky part though is to replace the existing favicon with the one that the user configures. The default favicon is introduced by the Document.cshtml template that can be found in the fallback SafeMode theme:

RegisterLink(new LinkEntry {
    Type = "image/x-icon",
    Rel = "shortcut icon",
    Href = Url.Content(
"~/modules/orchard.themes/Content/orchard.ico")});

The registered link then gets rendered a little farther in the same template:

<head> 
    <meta charset="utf-8" /> 
    <title>@Html.Title()</title> 
     @Display(Model.Head)
</head>

This code defines a "Head" zone in the current shape, which happens to be Layout, for which Document is just a wrapper. Don't worry if you're a little lost, that's not really the important part, I'm only explaining how I tracked down where I needed to act.

This "Head" zone is populated by the ResourceFilter as follows:

head.Add(_shapeFactory.Metas());
head.Add(_shapeFactory.HeadLinks());
head.Add(_shapeFactory.StylesheetLinks());
head.Add(_shapeFactory.HeadScripts());

It should become clear from this that the right shape to modify here is HeadLinks. We just need an event that is triggered right before that shape gets rendered, the "Displaying" event. We can do that from a shape table provider:

public class FaviconShapes : IShapeTableProvider {
    public void Discover(ShapeTableBuilder builder) {
        builder.Describe("HeadLinks")
            .OnDisplaying(shapeDisplayingContext => {
                // Do stuff here.
            });
    }
}

Now all we need to do is get the current list of links and get any favicon from it:

var resourceManager = workContext.Resolve<IResourceManager>();
var links = resourceManager.GetRegisteredLinks();
var currentFavicon = links
    .Where(l => l.Rel == "shortcut icon" &&
l.Type == "image/x-icon") .FirstOrDefault();

And then if we found one we can just replace its Href property, or if there isn't one already we create it:

// Modify if found
if (currentFavicon != default(LinkEntry)) {
    currentFavicon.Href = faviconUrl;
}
else {
    // Add the new one
    resourceManager.RegisterLink(new LinkEntry {
        Type = "image/x-icon",
        Rel = "shortcut icon",
        Href = faviconUrl
    });
}

And that's it. Share and enjoy!

The module (with full source code) can be downloaded from the Orchard Gallery:
http://orchardproject.net/gallery/Packages/Modules/Details/Vandelay-Favicon-1-0

Creating and maintaining Orchard translations

(c) Bertrand Le Roy 2010Many volunteers have already stepped up to provide translations for Orchard. There are many challenges to overcome with translating such a project.

Orchard is a very modular CMS, so the translation mechanism needs to account for the core as well as first and third party modules and themes.

Another issue is that every new version of Orchard or of a module changes some localizable strings and adds new ones as others enter obsolescence.

In order to address those problems, I've built a small Orchard module that automates some of the most complex tasks that maintaining a translation implies. In this post, I'll walk you through the operations I had to do to update the French translation for Orchard 1.0.

In order to make sure you translate all the first party modules, I would recommend that you start from a full source code enlistment. The reason is that I'll show how you can extract the default en-US translation from any source code enlistment. That enables you to create a translation that is even more up-to-date than what is currently on the site. Alternatively, you could start by downloading the current en-US translation. If you decide to do so, just skip the relevant paragraphs.

First, let's install the Orchard Translation Manager. I'm starting from a vanilla clone of the latest in the code repository. After you've setup the site, go into the dashboard and click on Gallery. Locate the Orchard Translation Manager in the list of modules and click "Install".

Once the module is installed, you need to enable its one feature by going into Configuration/Features and clicking "Enable" next to Vandelay.TranslationManager.Enabling the translation manager

We're done with the setup that we need in order to start our translation work. We'll now switch to the command-line and to our favorite text editor.

Open a command-line on the Orchard web site folder. I found the easiest way to do this is to do a SHIFT+right-click on the Orchard.Web folder in Windows Explorer and to click "Open command window here". Type bin\orchard to enter the Orchard command-line environment.

If you do a "help commands" you should see four commands in the list that came from the module we just installed: extract default translation, install translation, package translation and sync translation.

First, we're going to generate the default translation. Note that it is possible to generate that default translation for a specific list of modules and themes by using the /Extensions: switch, which should facilitate the translation of third party extensions, but in this tutorial we're going to generate it for the whole of the Orchard source code.

extract default translation /Output:\temp

This should have created an Orchard.en-us.po.zip file in the temp directory. Extract that archive into an orchard.po folder under \temp.

The next step depends on whether you have an existing translation that you want to update or not. If you do have an existing translation, just extract it into the same \temp\orchard.po directory. That should result in a file structure where you have the default en-US translation alongside your own. If you don't have an existing translation, just continue, the commands will be the same.

We are now going to synchronize those translations (or generate the stub for a new one if you didn't start from an existing translation).

sync translation /Input:\temp\orchard.po /Culture:fr-FR

After this command (where you should of course substitute fr-FR with the culture you're working on), we now have updated files that contain a few useful flags.

Open each of the .po files under the culture you are working on (there should be around 36) with your favorite text editor.

For all the strings that are still valid in the latest version, nothing changes and you don't need to do anything.

For all the strings that disappeared from the default culture, the old translation will still be there but they will be prefixed with the following comment:

# Obsolete translation

Conveniently, all the obsolete strings will be grouped at the end of the file. You can select all those and delete them.

For all the new strings, you will see the following comment:

# Untranslated string

This is where the hard work begins. You'll need to translate each of those new strings by entering the translation between the quotes in:

msgstr ""

Don't introduce hard carriage returns in the strings, just stay on one line (your text editor should do some reasonable wrapping so this shouldn't be a big deal).

Once you're done with a file, save it. Make sure, and this is very important, that your text editor is saving using the UTF-8 encoding. In Notepad, that setting can be found in the file saving dialog by doing a "Save As" rather than a plain "Save":Saving in UTF-8 in Notepad

When all the po files have been edited, you are ready to package the translation for submission (a.k.a. sending e-mail to the localization mailing list).

package translation /Culture:fr-FR
/Input:\temp\orchard.po /Output:\temp

You should now see a Orchard.fr-FR.po.zip file in temp that is ready to be submitted.

That is, once you've tested it, which can be done by deploying it into the site:

install translation \temp\orchard.fr-fr.po.zip

Once this is done you can go into the dashboard under Configuration/Settings and click on "Add or remove supported cultures for the site". Choose your culture and click "Add". You can go back to settings and set the default culture. Save.

You may now take a tour of the application and verify that everything works as expected:The site in French

And that's it really. Creating a translation for Orchard is a matter of a few hours. If you don't see a translation for your culture, please consider creating it.

Orchard shapeshifting

(c) Bertrand Le Roy 2010I've shown in a previous post how to make it easier to change the layout template for specific contents or areas. But what if you want to change another shape template for specific pages, for example the main Content shape on the home page?

Here's how.

When we changed the layout, we had the problem that layout is created very early, so early that in fact it can't know what content is going to be rendered. For that reason, we had to rely on a filter and on the routing information to determine what layout template alternates to add.

This time around, we are dealing with a content shape, a shape that is directly related to a content item. That makes things a little easier as we have access to a lot more information.

What I'm going to do here is handle an event that is triggered every time a shape named "Content" is about to be displayed:

public class ContentShapeProvider : IShapeTableProvider {
    public void Discover(ShapeTableBuilder builder) {
        builder.Describe("Content")
            .OnDisplaying(displaying => {
                // do stuff to the shape
            });
    }
}

This handler is implemented in a shape table provider which is where you do all shape related site-wide operations.

The first thing we want to do in this event handler is check that we are on the front-end, displaying the "Detail" version, and not the "Summary" or the admin editor:

if (displaying.ShapeMetadata.DisplayType == "Detail") {

Now I want to provide the ability for the theme developer to provide an alternative template named "Content-HomePage.cshtml" for the home page.

In order to determine if we are indeed on the home page I can look at the current site's home page property, which for the default home page provider contains the home page item's id at the end after a semicolon. Compare that with the content item id for the shape we are looking at and you can know if that's the homepage content item. Please note that if that content is also displayed on another page than the home page it will also get the alternate: we are altering at the shape level and not at the URL/routing level like we did with the layout.

ContentItem contentItem = displaying.Shape.ContentItem;
if (_workContextAccessor.GetContext().CurrentSite
.HomePage.EndsWith(';' + contentItem.Id.ToString())) {

_workContextAccessor is an injected instance of IWorkContextAccessor from which we can get the current site and its home page.

Finally, once we've determined that we are in the specific conditions that we want to alter, we can add the alternate:

displaying.ShapeMetadata.Alternates.Add("Content__HomePage");

And that's it really. Here's the full code for the shape provider that I added to a custom theme (but it could really live in any module or theme):

using Orchard;
using Orchard.ContentManagement;
using Orchard.DisplayManagement.Descriptors;

namespace CustomLayoutMachine.ShapeProviders {
  public class ContentShapeProvider : IShapeTableProvider {
    private readonly IWorkContextAccessor _workContextAccessor;

    public ContentShapeProvider(
IWorkContextAccessor workContextAccessor) {
_workContextAccessor = workContextAccessor; }
public void Discover(ShapeTableBuilder builder) { builder.Describe("Content") .OnDisplaying(displaying => { if (displaying.ShapeMetadata.DisplayType
== "Detail") {
ContentItem contentItem =
displaying.Shape.ContentItem; if (_workContextAccessor.GetContext()
.CurrentSite.HomePage.EndsWith( ';' + contentItem.Id.ToString())) {
displaying.ShapeMetadata.Alternates.Add(
"Content__HomePage"); } } }); } } }

The code for the custom theme, with layout and content alternates, can be downloaded from the following link:
Orchard.Themes.CustomLayoutMachine.1.0.nupkg

Note: this code is going to be used in the Contoso theme that should be available soon from the theme gallery.

More Posts