ILearnable .Net

July 13, 2010

Using T4 to generate strongly typed translations in episerver (+ how to fire T4 on builds)

Filed under: Uncategorized — andreakn @ 12:49
Tags: , ,

Problem
Typically when developing with EPiServer in the past I have created a helper method to provide a shorthand for accessing language strings:

public static class Lang
{
  public static string Translate(string key)
  {
    //call standard EPiServer API
  }
  public static string Translate(string key, string defaultValue)
  {
    //call standard EPiServer API, return defaultValue if no match found
  }
}

the trouble is the key parameter, it is on the form “/Forms/RegisterForm/Firstname” for instance, which means that every developer needs to know which language strings have been defined and get their full xpath from the language strings (no spelling errors, please), this makes discovering common strings a bit painful as Visual studio does not provide the handy navigator in the bottom for xml files as it does for .aspx files.

I wanted to point T4 at one (or more) language file(s) and get generated accessors so I could get intellisense when accessing language strings.

Solution
what I ended up with was this T4 code (which I put into a file called EPiLanguageAccessors.tt:

<#@ template debug="true" hostSpecific="true" #>
<#@ assembly name="System.Xml"#>
<#@ assembly name="System.Xml.Linq" #>
<#@ assembly name="System.Core" #> 
<#@ import namespace="System.Xml.Linq"#>
<#@ import namespace="System.Linq"#>
<#@ import namespace="System.Collections.Generic"#>


<#
int skipLevels = 2; //depends on xml file, for standard episerver use 2
string xmlFilePath = @"\lang\lang.xml"; 
string baseCodeGenNamespace = "Lang";
XDocument xdoc = XDocument.Load(System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+xmlFilePath);

GenerateLanguageAccessors(xdoc.Root, new List<string>(),baseCodeGenNamespace,skipLevels, new List<string>());
        
#>

<#+

 void GenerateLanguageAccessors(XNode xNode, List<string> ns, string baseNS, int skipLevels, List<string> cache)
        {
            if (xNode is XText)
            {
                if (ns.Count < skipLevels) return;
                string theNameSpace = string.Join(".", ns.Skip(skipLevels).Take(ns.Count - (skipLevels)).ToArray());
                if (string.IsNullOrEmpty(theNameSpace))
                {
                    theNameSpace = baseNS;
                }
                else
                {
                    theNameSpace = baseNS + "." + theNameSpace;
                }
                string theKey = "/" + string.Join("/", ns.Skip(skipLevels).ToArray());
                string s = "namespace " + theNameSpace + "{ public static partial class Get{ public static string Text(){ return Utils.LanguageUtil.Translate(\"" + theKey + "\");} public static string TextOrDefault(string defaultString){ return Utils.LanguageUtil.Translate(\"" + theKey + "\",defaultString);} } }";
               	if(!cache.Contains(theNameSpace))
                {
                    this.WriteLine(s);
               	 	cache.Add(theNameSpace);
                }
				 return;
            }
            if (xNode is XElement)
            {
                var xElement = xNode as XElement;
                var newNameSpace = ns.ToArray().ToList();
                newNameSpace.Add(xElement.Name.ToString());
                foreach (var subNode in xElement.Nodes())
                {
                    GenerateLanguageAccessors(subNode, newNameSpace, baseNS, skipLevels, cache);
                }
            }
        }
		#>

The top part of the .tt file is needed to reference the right stuff for the text transformation,
the middle part is where you specify the specifics of what you want, for instance skiplevel is needed to skip “/language/language” which is a part of the xml files, but not used for accessing strings, also here is where I define the relative path to the language file which is the “master” which will be used for code generation.
the bottom part is the algorithm which creates namespaces for each string with translation logic.

so where previously I would have to use a magic string in the call to LanguageUtil.Translate("/App/Registration/Step1/EulaText"); I can now call Lang.App.Registration.Step1.EulaText.Get.Text();

Getting it to run on each build

T4 out of the box runs the transformation each time the template file is changed. In our case that is not too useful, as we would rather have the transformation run each time the “master” language file is changed. AFAIK it isn’t possible to instruct T4 to fire when some custom file is changed, but you can get it to fire on each build.

There are a few options, for a run down you might want to head over to http://www.olegsych.com which is a goldmine of T4 information.
I found the simplest option to spread across a team (no need to install anything extra to the dev machines) is to create a file called FireT4OnBuild.targets, put it into the root of the project and populate it like this:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="ExecuteT4Templates">
        <ItemGroup>
            <T4Templates Include=".\**\*.tt" />
        </ItemGroup>
         <Exec
            WorkingDirectory="C:\Program Files (x86)\Common Files\microsoft shared\TextTemplating\10.0\"
            Command="TextTransform &quot;%(T4Templates.FullPath)&quot; -out &quot;%(T4Templates.RootDir)%(T4Templates.Directory)%(T4Templates.Filename).cs&quot; " /> 
    </Target>
</Project>

You might want to change the path for the working directory (my setup is for .net 4.0 running on windows 7 (64bit)
if the devs use different setups you might want to copy the texttemplating files to a common path (SolutionDir/Lib/T4 for instance) and reference that path

Then you need to unload the project containing the transformation (I’m guessing your xxxxx.Web.csproj ) and edit the .csproj file (right click => unload, then right click => edit )
and add the line

 <Import Project="$(MSBuildProjectDirectory)\FireT4OnBuild.targets" />

in the bottom of the file (put it after the import of Microsoft.CSharp.targets) and changing the defaulttargets to “ExecuteT4Templates;Build”

Now T4 should be firing every time you touch the .tt file(s) and also every time you perform a build. Since ExecuteT4Templates is put before Build the generated code will be in place in time for the build

Advertisements

3 Comments »

  1. Very interesting and clever use of T4. Will be really interesting to try this out!

    Comment by Joel Abrahamsson — July 13, 2010 @ 14:21 | Reply

  2. […] Normally the code(config) generation will only be triggered by a modification to the .tt file, but there is an easy way to get T4 to fire on every build. […]

    Pingback by Using T4 to manage config files for complex deployment scenarios « Andreas’ code blog — August 6, 2010 @ 12:37 | Reply

  3. […] link: Using T4 to generate strongly typed translations in episerver (+ how to fire T4 on builds) This entry was posted in EPiServer articles and tagged api, defaulttargets, generate, generated, […]

    Pingback by Using T4 to generate strongly typed translations in episerver (+ how to fire T4 on builds) | EPiServerCMS — September 30, 2010 @ 00:46 | Reply


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: