Optimizing Dataverse Localization with Microsoft Translator Text API

Optimizing Dataverse Localization with Microsoft Translator Text API

Microsoft Translator Text API "is easy to integrate in your applications, websites, tools, and solutions. It allows you to add multi-language user experiences in more than 60 languages, and can be used on any hardware platform with any operating system for text-to-text language translation. The Translator Text API is part of the Azure Cognitive Services API collection of machine learning and AI algorithms in the cloud, and is readily consumable in your development projects."

There are many usage scenarios for it, but in this article I will show how it can be easily integrated to enhance localization capabilities of Dataverse (Dynamics 365) - based system. I will use a model app for my example, but the same principles are easily transferable to a canvas app or to a PowerApps Portal.

Dynamics 365 system localization is based on providing custom strings for screen resources in each supported local language. After a localization is enabled, the account form for an English-speaking system user might will look like this:

No alt text provided for this image

and the same model app form for someone whose language is set to French will display French-localized labels (note that custom optionsets and Description are not translated):

No alt text provided for this image

Full localization, especially if your system needs to support multiple secondary languages, is a be tedious and expensive process, and might require significant future maintenance. Imagine, for example, that besides main UI elements, you have several dozens (or hundreds) of optionsets - you would need to provide a translation for each option's text into each supported language, and continue doing this while available options are changing. Additionally, the data itself cannot be localized. If one system user enters the value of a text field in French, an English-speaking user might not be able to understand it.

Translator Text API is a great tool to implement the happy middle ground solution where the main screen UI elements are localized to enable stricter control of the system's look and feel, while the rest of the localization occurs at run time. For this example, I will show how a simple form-based JavaScript snippet may be used to control the data and attribute translations.

To start with, I have created a subscription to the Translator Text API and received a personalized access key unique to my subscription. This key is required on each call to the Translator Text API.

Once this is done, I've created a new JavaScript Web Resource and registered a method to run during the Load event of an Account form:

No alt text provided for this image

In this resource I start with declaring a settings object with the target languages, my application key and the endpoint. In a production system I would probably store my settings elsewhere rather than hardcoding them. I could also retrieve target language by querying the current user's information and I could infer source language by examining the properties of the app itself (or I could rely on Translator API's capabilities to auto-determine the language). I define a set of controls I'd like to process and invoke translateControlValues function on each.

No alt text provided for this image

In the body of translateControlValues function, I first retrieve the data to translate using getAttributeWithValues function. For the purposes of this example, I will only translate values of Text fields and Option texts of a few OptionSet fields.?

The getAttributeWithValues function takes a field's schema name and returns a JSON object with a collection of source text strings. The Translator API expects the input data as arrays of {'Text': 'Text to translate'} objects.

No alt text provided for this image

Once I got my data to translate, I POST to the Translator API endpoint supplying my auth key and the source data. One optimization I could achieve here is to try to make this call chunkier by bundling together translation requests for multiple fields. Note that at the moment, Translator API has following limitations: The array can have at most 100 elements; The entire text included in the request cannot exceed 5,000 characters including spaces.

No alt text provided for this image

Within the translationCallback callback function, I replace each of the optionset's Option text values with the translation (if the field is Optionset) or display the translated text in the Label of the text field.

No alt text provided for this image

With the help of on-demand translation, my custom optionsets and text fields get translated to the target language:

No alt text provided for this image

This approach can be used to build many additional enhancements. For example, I could add a ribbon button that would translate the data from the control under focus on demand or create a split-panel display that would show original data on the left and mirror translated controls on the right.

Full code listing

var Sdk = window.Sdk || {};
(function () {
this.onLoad = function (_context) {
? $(document).ready(function () {
? ? var context = _context.getFormContext();


? ? try {
? ? ? context.ui.clearFormNotification('translationerror');


? ? ? // declare settings for the translator
? ? ? var
? ? ? ? settings = {
? ? ? ? ? fromLang: 'en',
? ? ? ? ? toLang: 'fr',
? ? ? ? ? apiKey: '00000000000000000000000000',
? ? ? ? ? endPoint: 'https://api.cognitive.microsofttranslator.com/translate?api-version=3.0'
? ? ? ? },
? ? ? ? controlsToTranslate = ['description', 'gc_businessorganizationtype', 'gc_perception', 'gc_socioeconomicdata', 'gc_companytype'];


? ? ? controlsToTranslate.forEach(controlName => translateControlValues(context, settings, controlName));
? ? }
? ? catch (ex) {
? ? ? context.ui.setFormNotification('Error while translating: ' + ex, 'ERROR', 'translationerror');
? ? }
? });


? // prepare and execute POST request; update controls with translated text
? // get values for the control we want to translate; and create the request
? // API expectes data in this format: [{'Text':'Hello world'}]
? function translateControlValues(context, settings, controlName) {
? ? var attrWithValues = getAttributeWithValues(context, controlName),
? ? ? content = JSON.stringify(attrWithValues.values),
? ? ? url = settings.endPoint +
? ? ? ? ? ? '&from=' + settings.fromLang +
? ? ? ? ? ? '&to=' + settings.toLang,
? ? ? req = new XMLHttpRequest();


? ? req.open('POST', url, true);
? ? req.setRequestHeader("Ocp-Apim-Subscription-Key", settings.apiKey);
? ? req.setRequestHeader("OData-MaxVersion", "4.0");
? ? req.setRequestHeader("OData-Version", "4.0");
? ? req.setRequestHeader("Accept", "application/json");
? ? req.setRequestHeader("Content-Type", "application/json; charset=utf-8");


? ? req.onreadystatechange = function () {
? ? ? translationCallback(req, context, controlName, attrWithValues);
? ? };


? ? req.send(content);
? }


? // get values from OptionSet or Text attributes and fill out json object with collection??
? // of { 'Text': 'Text to translate' } objects
? function getAttributeWithValues(context, controlName) {
? ? var attrWithValues = {
? ? ? ? ? name: controlName,
? ? ? ? ? type: context.getControl(controlName).getControlType(),
? ? ? ? ? values: []
? ? ? ? },
? ? ? attr = context.getAttribute(controlName);


? ? if (attrWithValues.type === 'optionset') { // get OS values and add to value collection
? ? ? attr.getOptions().forEach(el => attrWithValues.values.push({ 'Text': el.text }));
? ? }
? ? else { // assume it is a text field
? ? ? attrWithValues.values.push({ 'Text': attr.getValue() });
? ? }


? ? return attrWithValues;
? }


? // update control values with translated text
? function translationCallback(xr, context, controlName, attrWithValues) {
? ? if (xr.readyState !== 4) { return; }


? ? if (xr.status === 200) {
? ? ? // response is an array of Translation arrays, e.g.?
? ? ? //[{ "translations": [{ "text": "Bonjour, comment tu t’appelles ?", "to": "fr" }] }]}]
? ? ? var translations = JSON.parse(xr.responseText),
? ? ? ? attribute = context.getAttribute(controlName),
? ? ? ? control = context.getControl(controlName);


? ? ? // replace control value with translated text
? ? ? if (attrWithValues.type === 'optionset') {
? ? ? ? // update OS values by looping through each translation?
? ? ? ? // and replacing corresponding value
? ? ? ? var attributeOptions = attribute.getOptions();
? ? ? ? control.clearOptions();	// remove orignal set


? ? ? ? for (var i = 0; i < translations.length; i++) {
? ? ? ? ? // replace original Option text with translated
? ? ? ? ? attributeOptions[i].text = translations[i].translations[0].text;
? ? ? ? ? control.addOption(attributeOptions[i]);
? ? ? ? }
? ? ? }
? ? ? else {
? ? ? ? control.setLabel(translations[0].translations[0].text);
? ? ? }
? ? }
? ? else {
? ? ? context.ui.setFormNotification(xr.statusText, 'ERROR', 'translationerror');
? ? }
? }
};
}).call(Sdk);        

要查看或添加评论,请登录

Dmitri Riz的更多文章

社区洞察

其他会员也浏览了