microsoft / BotBuilder-Samples

Welcome to the Bot Framework samples repository. Here you will find task-focused samples in C#, JavaScript/TypeScript, and Python to help you get started with the Bot Framework SDK!
https://github.com/Microsoft/botframework
MIT License
4.39k stars 4.88k forks source link

Translation Samples needs to translate adaptive cards #1723

Open cleemullins opened 5 years ago

cleemullins commented 5 years ago

CSS Sees issues come in related to translation, where cards (adaptive, etc) are not being translated.

Consider updating this sample to include an Adaptive Card translation.

mdrichardson commented 5 years ago

Just some quick input. There's two routes we can go with this (that I can think of):

  1. They'd need to use the AdaptiveCards NuGet package to build them with translated strings. Something like:
AdaptiveCard card = new AdaptiveCard();

card.Body.Add(new AdaptiveTextBlock() 
{
    Text = translatedString,
    Size = AdaptiveTextSize.ExtraLarge
});

or,

  1. If users want to use a JSON card, we'd need a recursive nested JSON search similar to this to manually replace the strings
v-kydela commented 5 years ago

I have encountered this in IcM form twice and I wrote a C# function to translate Adaptive Cards so I'll paste it here:

The nested functions may appear a little confusing but I thought it would be better to have one self-contained function rather than have multiple functions.

public async Task<object> TranslateAdaptiveCardAsync(object card, string targetLocale, CancellationToken cancellationToken = default)
{
    var propertiesToTranslate = new[] { "text", "altText", "fallbackText", "title", "placeholder", "data" };

    var cardJObject = JObject.FromObject(card);
    var list = new List<(JContainer Container, object Key, string Text)>();

    void recurseThroughJObject(JObject jObject)
    {
        var type = jObject["type"];
        var parent = jObject.Parent;
        var grandParent = parent?.Parent;
        // value should be translated in facts and Input.Text, and ignored in Input.Date and Input.Time and Input.Toggle and Input.ChoiceSet and Input.Choice
        var valueIsTranslatable = type?.Type == JTokenType.String && (string)type == "Input.Text"
            || type == null && parent?.Type == JTokenType.Array && grandParent?.Type == JTokenType.Property && ((JProperty)grandParent)?.Name == "facts";

        foreach (var key in ((IDictionary<string, JToken>)jObject).Keys)
        {
            switchOnJToken(jObject, key, propertiesToTranslate.Contains(key) || (key == "value" && valueIsTranslatable));
        }
    }

    void switchOnJToken(JContainer jContainer, object key, bool shouldTranslate)
    {
        var jToken = jContainer[key];

        switch (jToken.Type)
        {
            case JTokenType.Object:

                recurseThroughJObject((JObject)jToken);
                break;

            case JTokenType.Array:

                var jArray = (JArray)jToken;
                var shouldTranslateChild = key as string == "inlines";

                for (int i = 0; i < jArray.Count; i++)
                {
                    switchOnJToken(jArray, i, shouldTranslateChild);
                }

                break;

            case JTokenType.String:

                if (shouldTranslate)
                {
                    // Store the text to translate as well as the JToken information to apply the translated text to
                    list.Add((jContainer, key, (string)jToken));
                }

                break;
        }
    }

    recurseThroughJObject(cardJObject);

    // From Cognitive Services translation documentation:
    // https://docs.microsoft.com/en-us/azure/cognitive-services/translator/quickstart-csharp-translate
    var requestBody = JsonConvert.SerializeObject(list.Select(item => new { item.Text }));

    using (var request = new HttpRequestMessage())
    {
        var uri = $"https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to={targetLocale}";
        request.Method = HttpMethod.Post;
        request.RequestUri = new Uri(uri);
        request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
        request.Headers.Add("Ocp-Apim-Subscription-Key", _key);

        var response = await _client.SendAsync(request, cancellationToken);

        response.EnsureSuccessStatusCode();

        var responseBody = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<TranslatorResponse[]>(responseBody);

        if (result == null)
        {
            return null;
        }

        for (int i = 0; i < result.Length && i < list.Count; i++)
        {
            var item = list[i];
            var translatedText = result[i]?.Translations?.FirstOrDefault()?.Text;

            if (!string.IsNullOrWhiteSpace(translatedText))
            {
                // Modify each stored JToken with the translated text
                item.Container[item.Key] = translatedText; 
            }
        }

        // Return the modified JObject representing the Adaptive Card
        return cardJObject;
    }
}

image

I would like to extend a word of caution. If you know everything your bot can say ahead of time, which is the case for almost all bots, you should translate that text ahead of time and store the results in resource files. This will reduce the amount of calls you have to make to the translator service and also give you better translations because you will have more control over them. This is especially true if you have any text that is hard for the translator service to interpret, like complicated markdown.

The translator service is best used for incoming messages. If you must use it for outgoing messages, this Adaptive Card translator function should help you. But know that Adaptive Cards are complex by nature, and you may want to tweak this code to suit your own needs if there are some components of Adaptive Cards that you don't want to be translated, or if there are edge cases I haven't considered where my code can't handle certain cards.

v-kydela commented 5 years ago

I have also included Adaptive Card translations in my Bot Builder Community proposal: https://github.com/BotBuilderCommunity/botbuilder-community-dotnet/issues/137

madbeher commented 4 years ago

Can we use the same function to translate hero cards also ?

Thanks..

v-kydela commented 4 years ago

@madbeher - No, the function is specific to Adaptive Cards. I'd need to make different functions to translate Bot Framework cards. Please comment on the Bot Builder Community issue with any requests.