Monday, 23 June 2008

Multi-lingual SSRS reports

So you want to make your reports multi-lingual, sensitive to each user's browser language preference. Wow! That sounds like a big job. That's what I thought first of all, but there was a requirement, so a bit of investigation later, it seems like things have got a bit easier...

First of SSRS reports expose a global parameter [User!Language] which reflects the user's primary language (top of list in IE). This is very helpful because it's not possible to find this out client-side in Javascript (after much trying!). It's only available as a server-side parameter.

Secondly, thanks to Google's TranslateAPI, you can automate a 'rough-cut' translation for the labels and tooltips which you will want to be language-sensitive.

Thirdly, in reports you can substitute your 'hard-coded' label for an embedded function call to retrieve something that is language-sensitive. Although it's a little annoying that you must pass into this function any references as the functions are operating in a 'sandbox'. Anyway, A decision I made was to store all the translations in the report RDL itself, rather than in the database - this might not be best for everyone. The translations can be stored as hidden parameters and retrieved by the embedded function.

eg. =Code.GetLabel(User!Language, Parameters!Labels, "Contact owner")

You've got all the tips now, but it still seems some work to do a number of reports. So the job really comes down to mechanising the whole operation, and for that I can show some code I used. Credit must go to all the sources I used to find most of the techniques, of which there were several.

Anyway I knocked up a utility to load up the RDL DOM, make a nodelist of the labels and tooltips and substitute the function call, whilst taking the previously existing (en) text, 'Googlising' it into 5 other languages and storing it as hidden parms.

Voila. Enjoy!

XmlDocument xml = new XmlDocument();
xml.Load(sFile);
//temporarily remove default namespace to avoid excess references
//also simplifies the code considerably!
string defns = xml.DocumentElement.NamespaceURI;
xml.LoadXml(xml.OuterXml.Replace("xmlns=\"" + defns + "\" ", ""));
XmlNode code = xml.SelectSingleNode("//Code");
if (code == null)
{//then make this node
XmlNode ds = xml.SelectSingleNode("//DataSets");
code = xml.CreateElement("Code");
xml.DocumentElement.InsertBefore(code, ds);
}
if (! code.InnerText.Contains("Public Function GetLabel"))
{//then add required function
code.InnerText += "Public Function GetLabel(Lang as String, P as Parameter, Label as String) as String" + Environment.NewLine;
code.InnerText += "Dim i As Integer" + Environment.NewLine;
code.InnerText += "For i = 0 to Ubound(P.Value)" + Environment.NewLine;
code.InnerText += "If (P.Value(i) = Left(Lang,2) + \"_\" + Label) Then Return P.Label(i)" + Environment.NewLine;
code.InnerText += "Next i" + Environment.NewLine;
code.InnerText += "Return Label" + Environment.NewLine;
code.InnerText += "End Function";
}

XmlNode parms = xml.SelectSingleNode("//ReportParameters");
XmlNode parm = parms.SelectSingleNode("//ReportParameter[@Name='Labels']");
if (parm == null)
{//then make this node
parm = xml.CreateElement("ReportParameter");
parms.AppendChild(parm);
XmlAttribute nm = xml.CreateAttribute("Name");
nm.Value = "Labels";
parm.Attributes.Append(nm);
parm.InnerXml = "String";
parm.InnerXml += "";
parm.InnerXml += "true";
parm.InnerXml += "";
parm.InnerXml += "true";
}
XmlNode defvals = parm.SelectSingleNode("DefaultValue/Values");
XmlNode valvals = parm.SelectSingleNode("ValidValues/ParameterValues");
string[] lang = { "en", "it", "fr", "de", "nl", "es" };
Language[] lenum = { Language.English, Language.Italian, Language.French, Language.German, Language.Dutch, Language.Spanish };
XmlNodeList lbls = xml.SelectNodes("//Textbox/Value|//Textbox/ToolTip");
foreach (XmlNode lbl in lbls)
{
if (! lbl.InnerText.StartsWith("="))
{
string def = lbl.InnerText;
lbl.InnerText = "=Code.GetLabel(User!Language, Parameters!Labels, \"" + def + "\")";

for (int i = 0; i < 6; i++)
{
XmlNode defval = xml.CreateElement("Value");
defval.InnerText = lang[i] + "_" + def;
defvals.AppendChild(defval);
XmlNode valval = xml.CreateElement("ParameterValue");
valval.InnerXml = "" + lang[i] + "_" + def + "";
valval.InnerXml += "";
valvals.AppendChild(valval);
}
}
}
//add back the default namespace
XmlAttribute rdef = xml.CreateAttribute("xmlns");
rdef.Value = defns;
xml.DocumentElement.Attributes.Prepend(rdef);
xml.Save(sFile);

Programmatically adding reports in MSCRM v4

New in v4, lots more objects are now system entities within MSCRM, including REPORTS. This means that you have lots more control and flexibility in manipulating reports in code, so I was surprised that I couldn't find good examples for uploading them. In case anyone else is looking for an example, I'm posting this...

Back in v3 we used publish.config as a convenient way of itemising the reports of interest and how we wanted them installed, so I have used the same file, adding only one more attribute [@parentname] as v4 is more picky in this regard.

You will notice I'm using the xpath navigator rather than the xmldocument in order to pre-sort the reports and ensure that the parentreports get created first! There are probably better/more elegant ways of doing some parts for which comments are gladly received.

Enjoy!

Addendum:You need to set the languagecode in order to get them to show in all views, plus there are some useful other params for maintaining/updating the reports which could be added as attributes to the config file.

rpt.languagecode = new CrmNumber(1033);
rpt.signatureid = new UniqueIdentifier(Guid.NewGuid());
rpt.signaturedate = new CrmDateTime("2008/06/23");
rpt.signaturemajorversion = new CrmNumber(4);
rpt.signatureminorversion = new CrmNumber(0);
rpt.signaturelcid = new CrmNumber(1033);



string cfgfile = "publish.config";
string rptfile = "";

#region Open config and sort reports

//open the config file and sort the report nodes
XPathDocument doc = new XPathDocument(cfgfile);
XPathNavigator nav = doc.CreateNavigator();
XPathExpression xrpts = nav.Compile("//report");
xrpts.AddSort("@parentname", XmlSortOrder.Ascending, XmlCaseOrder.None, "", XmlDataType.Text);

#endregion

foreach (XPathNavigator xrpt in nav.Select(xrpts))
{//load each report inc parent link and related as necessary
rptfile = xrpt.GetAttribute("filename", "");
report rpt = new report();

#region Set report parameters

using (StreamReader reader = new StreamReader(rptfile))
{
rpt.bodytext = reader.ReadToEnd();
}
rpt.iscustomreport = new CrmBoolean(false);
rpt.ispersonal = new CrmBoolean(false);
rpt.isscheduledreport = new CrmBoolean(false);
rpt.name = xrpt.GetAttribute("name", "").ToString();
rpt.description = xrpt.GetAttribute("description", "").ToString();
rpt.reporttypecode = new Picklist(ReportTypeCode.ReportingServices);
rpt.filename = rptfile;

#endregion

#region Set the parent link if necessary

string pn = xrpt.GetAttribute("parentname", "");
if (pn != "")
{//get the Id of the first matching parent report
FilterExpression flt = new FilterExpression();
flt.Conditions.Add(new ConditionExpression("name", ConditionOperator.Like, pn));
QueryExpression qry = new QueryExpression(EntityName.report.ToString());
qry.Criteria = flt;
report prpt = (report)service.RetrieveMultiple(qry).BusinessEntities[0];
rpt.parentreportid = new Lookup(EntityName.report.ToString(), prpt.reportid.Value);
}

#endregion

Guid rptId = service.Create(rpt);

#region Execute SetReportRelatedRequest
SetReportRelatedRequest rrReq = new SetReportRelatedRequest();
rrReq.ReportId = rptId;

#region Set the categories for the report

string[] cats = xrpt.GetAttribute("category", "").Split(';');
ArrayList catsi = new ArrayList(); int tmp = 0;
for (int i = 0; i < cats.Length; i++)
{
int.TryParse(cats[i], out tmp);
catsi.Add(tmp);
}
rrReq.Categories = (int[])catsi.ToArray(typeof(int));

#endregion

#region Set the relationships for the report

string[] rents = xrpt.GetAttribute("relatedentity", "").Split(';');
ArrayList rentsi = new ArrayList();
if (rents.Length != 0)
{
for (int i = 0; i < rents.Length; i++)
{//get the OTC for each related entity
RetrieveEntityRequest req = new RetrieveEntityRequest();
req.RetrieveAsIfPublished = true;
req.LogicalName = rents[i];
req.EntityItems = EntityItems.EntityOnly;
RetrieveEntityResponse res = (RetrieveEntityResponse)mservice.Execute(req);
EntityMetadata entMdata = res.EntityMetadata;
rentsi.Add(entMdata.ObjectTypeCode.Value);
}
}
rrReq.Entities = (int[])rentsi.ToArray(typeof(int));

#endregion

#region Set the visibility for the report

string[] viss = xrpt.GetAttribute("showin", "").ToString().Split(';');
ArrayList visi = new ArrayList();
for (int i = 0; i < viss.Length; i++)
{
switch (viss[i])
{
case "reports": visi.Add(ReportVisibilityCode.ReportsGrid); break;
case "entity": visi.Add(ReportVisibilityCode.Grid); break;
case "form": visi.Add(ReportVisibilityCode.Form); break;
}
}
rrReq.Visibility = (int[])visi.ToArray(typeof(int));

#endregion

// Execute the request.
SetReportRelatedResponse reportRelatedResponse = (SetReportRelatedResponse)service.Execute(rrReq);

#endregion

}