Dynamic 365 CE Web Resource Localization

Introduction

Since very long I was thinking of starting my own blog site, but due to other priorities and blockers it always took last position on my TODO list. But today I will be starting with this blog where I will be discussing a feature of Dynamic 365 V9.X which is not used very often but is very critical when it comes down to client who supports multi-lingual employees and clients.

Recently in my project I came across a requirement to have bilingual content (English and French) on HTML Web-Resources which was added to the Model Driven App form. Before V9 there was no out of the box solution for this requirement. But I have seen many different implementations such as in one implementation content was hardcoded in both the languages and in other implementation there where two different web-resources with English and French hardcoded string content and depending upon the current logged in user’s preferred language correct web resources is shown and other is hidden using JavaScript api.  In my opinion the first approach of having content hardcoded in the web resource for both the languages can work some cases  but not in all, think of an scenario where you may have to display multiline text, which might not be the good user experience and also since the text is hardcoded any updates to text must be directly updated in the web resource. Second option will require lot of development and is difficult to maintain because you might end up with lot of web resources serving same purpose. But because there was no out of the box solution before V9 these solutions used to work depending upon the requirement.

To provide the out of box solution Microsoft introduced String RESX Web Resource from Dynamic 365 CE V9.  Lets see what are RESX files and how these can be used to implemented localization to our web resource.

RESX web resources contain the keys and localized string values for a single language defined using the RESX XML format. RESX is the common format for defining localized resources for .Net applications, but when these files are published to Dataverse as web resources it is converted into JSON format. This JSON file is then used by the application to get requested string value.

Here are the high level steps that describes how we can use RESX files for content localization in HTML or JavaScript web-resource.

  • Create String Web Resource file with .resx extension for each language with same Keys and translated string values.
  • For each RESX create RESX web resource with proper naming convention web-resourcename.languagecode.resx eg: ps_instructions.1036.resx
  • Add these RESX files as Web Resource dependencies to JavaScript web resource in which localization is required by JavaScript or by HTML type web resource.
  • Used JavaScript api utility method Xrm.Utility.getResourceString(webResourceName, key) to get the localized string based on the user’s preferred language and replace the default value using JavaScript.  

Step By Step Instruction

  • Step 1: Using Visual Studio add new “Resource File” to existing or new project as show below
  • Step 2: Save this file with following naming convention  web-resourcename.languagecode.resx, in this case I used instructions.1036.resx and instructions.1033.resx.
  • Step 3: Add localized values and corresponding keys using RESX design viewer in Visual Studio, see below
  • Step 4: Now login into https://make.powerapps.com/ and then create new web resource by navigate to Solution > New > Other > Web Resource
  • Step 5:  Here are the details while creating RESX web resource as show below:
    • Name: Use the naming conventions web-resourcename.languagecode.resx, in this case I used ps_instructions.1036.resx. Naming convention must be properly followed to avoid any issues.
    • Display Name: instructions.1036.resx
    • Description: Meaningful description for the web-resource
    • Type: String(RESX)
    • Language: Select appropriate language, in this case I selected “French”
    • Upload File: Select instruction.1036.resx file created in Step 1 and 2.
  • Step 6: Hit “Save” and then “Publish”.
  • Step 7: Repeat Step 1 through 7 for all required languages. In this example I repeated these steps for English language (1033). I did this just because I did not wanted to use hardcoded values in HTML web resource.
  • Step 8: Now we need to make sure we load String RESX files ONLOAD of the form. For this we need to add these files as dependencies to our the web resource in which we would like to perform localization. In this example we as are using HTML web resource so we will not be able add these files as dependencies to the HTML web resource because these files will not be available when the form is loaded. To get around this issue follow these sub-steps:
    1. Create a JavaScript web resource called “ps_localizationdependencies.js” (you can name whatever you want) with a stub function “function loadForm(){};
    2. Search and add all the RESX web resources created in Step 5,6 and 7 one by one as dependencies to this JavaScript web resource as show below:
  • Step 9: Add  “ps_localizationdependencies.js” as Form Libraries under Form Properties for the the form on which you are using the HTML web resource, in this example I am using “ps_instruction.html” and the form that I am using is “Contact” form in Contact Table. 
  • Step 10: Add OnLoad Event for  “ps_localizationdependencies.js” and invoke sub loadform() function as show in the screenshot.
  • Step 11: Now write the JavaScript to use RESX files to localized string values. If localization is required JavaScript  web resource then you might want to add this script in “ps_localizationdependencies.js” itself (for this example, you might have different script). If localization is required for HTML web resource then add script in the HTML web resource. For this example I will update the “ps_instruction.html” web resource as below
<html>
    <head>
        https://code.jquery.com/jquery-3.6.0.min.js
    </head>
    <body style="overflow-wrap: break-word;">
        <section>
            <h3><span id="instruction_title"></h3>
            <p>
                <span id="instruction"></span>
            </p>
        </section>    
    </body>
    
 <style>
    body    { font-family: "Segoe UI Regular", SegoeUI, "Segoe UI" }
 </style>
 <script>
     
        function GetTranslatedString(webResourceName, key)
        {
            return parent.Xrm.Utility.getResourceString(webResourceName, key);
        };
        function UpdateStringLiterals()
        {
            var instruction = GetTranslatedString("ps_instructions", "Instruction");
            var instruction_header = GetTranslatedString("ps_instructions", "InstructionHeading");
            $("#instruction").text(instruction);
            $("#instruction_title").text(instruction_header);
        }
        $(function ()
        {
            UpdateStringLiterals();
        }); 
            
 </script>
</html>

Lets break down what I did in the above code.

  1. Added reference to CDM JQuery script tag, this is an optional step. If you don’t want to use JQuery then you can just use OOB JavaScript and call the function on the load of “ps_instruction.html” web resource.
  2. Added Script tag with following functions:
    • GetTranslatedString : This function is used to get the String from RESX files by passing two parameters “webResouceName” and “key”. It uses OOB  Xrm.Utility.getResourceString api function to retrieve string value from correct RESX file depending upon the current user’s preferred language.
    • UpdateStringLiterals : This function performs two task, one to get the string from RESX resource file and secondly it update required element in HTML.

Outcome

Here is the result of implementing the steps for “Contact” from for the HTML web resource ps_instruction.html.

  • If the preferred language for user is set to be “English” below is the result seen on the “Contact” from:
  • If the preferred language for user is set to be “French” below is the result seen on the “Contact” from:
  • To know more on how to enable languages in your environment please visit here
  • To know more on how to change preferred language of a user please visit here

Things to consider

  • Default language for Organization is consider as a fallback language option for RESX resources. What that means if RESX file is missing for a preferred language then organization default language will return the string values.
  • If “Key” is not found in RESX file then Xrm.Utility.getResourceString function will return null.

Final Remarks

Before Dynamic 365 V9 localization tasks around web resources were pretty challenging from development perspective as there was no out of the box efficient option to deal with these requirements. But since Xrm.Utility.getResourceString function is added localization for web resources has become very easy to achieve.

Hope this is helpful. Please leave comments or if you have any questions, I will be more than happy to respond.

Streamlining automation of Word documents generation using Word Document Templates in Model Driven apps

Automating the generation of Word documents through the use of Word Document Templates in Model Driven Power Apps is a common task that has been possible to do for some time now using the Populate a Microsoft Word template action from the Word Online (Business) connector in Power Automate. There are countless posts out there that explain how this is done.

However, this is a bit of a tedious process as it requires manually specifying in the flow step the value for each content placeholder in the document template. It also involves having to store the Word Document Template in a location where it can be retrieved by the flow to be able to use it, for example OneDrive or SharePoint.

In the rest of this post I describe what I believe is a more streamlined way to go about automating the generation of Word documents. Not too long ago I found that there is an endpoint that can be used to generate Word documents from Word Document Templates. The endpoint is composed of the environment URL followed by /_grid/print/print_data.aspx. This allows the generation of Word documents without having to deal with the nuances that I mention above. In other words, the document template only needs to be present in the Dataverse environment and all data is automatically populated in the Word document with no need to specify what data goes in what content placeholder within the document template.

With this in mind, I created a Power Automate flow that uses the Invoke an HTTP request action from the HTTP with Azure AD connector in Power Automate to make a call to this endpoint to generate the Word document based on a given Word Document template. Then I used the return value which is the content of the word document to create the Word file in OneDrive. Here’s what the flow looks like with the details to follow:

Flow Steps

Step 1 is the trigger of the flow which in this case is when a record is added, modified or deleted.

Step 2 saves the environment URL into a variable that’s manually populated for later use. It would be your environment base URL, for example: https://orge092ce7b.crm3.dynamics.com. Normally you would have an automated way to obtain the environment URL for ALM purposes, but this will do for this post.

Step 3 uses the aforementioned Invoke an HTTP request action to first obtain the object type code of the table for which the document is being generated. This is needed later when calling the endpoint.

Step 4 retrieves the Document Template to be used to generate the word document based on the name of the template

Step 5 once again uses the Invoke an HTTP request action, but this time it is to generate the Word document content using our featured endpoint. The following needs to be provided for this call:

Accept Header:

text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8

Accept-Language Header:

en-US,en;q=0.8

Content-Type Header:

aplication/x-www-form-urlencoded

Body of the request:

  • exportType: MergeWordTemplate
  • selectedRecords: This needs to be the encoded set of row IDs for which the document will be generated. In our case, it’s just the one record. However it still needs to be provided as an encoded array of GUIDs. In javascript you would obtain it like this: encodeURIComponent(JSON.stringify(['id here'])). In Power Automate however, there’s nothing like JSON.stringify. You get around that by providing your row ID flanked by the following set of characters: %5B%22 and %22%5D where %5B is the encoded string for an opening square bracket, %5D is the encoded string for a closing square bracket and %22 is the encoded string for quotes.
  • associatedentitytypecode: This is where the entity type code that we got on step 3 needs to be provided
  • TemplateId: This is the GUID of the document template we got in step 4
  • TemplateType: 9940 (9940 is global and 9941 is personal)

Putting it all together, the body of the request would be as follows. The pieces in bold would be the dynamic values obtained from the previous steps.

exportType=MergeWordTemplate&selectedRecords=%5B%22@{triggerOutputs()?['body/contactid']}%22%5D&associatedentitytypecode=@{outputs('Invoke_an_HTTP_request_to_get_Object_Type_Code_of_Contact_Table')?['body']?['value']}&TemplateId=@{first(outputs('List_rows_for_the_Document_Template_Table')?['body/value'])?['documenttemplateid']}&TemplateType=9940

Finally, step 6 uses the output of step 5 as the File content to generate the Word document in One Drive.

I hope you find this tip useful.