{"id":31275,"date":"2020-12-15T09:00:46","date_gmt":"2020-12-15T16:00:46","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=31275"},"modified":"2024-01-12T11:31:39","modified_gmt":"2024-01-12T19:31:39","slug":"localize-net-applications-with-machine-translation","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/localize-net-applications-with-machine-translation\/","title":{"rendered":"Localize .NET applications with machine-translation"},"content":{"rendered":"<p>In this post, I&#8217;m going to introduce you to a GitHub Action that creates machine-translations for .NET localization. GitHub Actions allow you to build, test, and deploy your code right from GitHub, but they also allow for other workflows. You can perform nearly any action imaginable against your source code as it evolves. With the Machine Translator GitHub Action, you configure a workflow to automatically create pull requests as translation source file change.<\/p>\n<p>You can use <a href=\"https:\/\/docs.microsoft.com\/aspnet\/core\/blazor\/globalization-localization?view=aspnetcore-3.1#blazor-webassembly&amp;WT.mc_id=dapine\">localization with Blazor WebAssembly (Wasm)<\/a> to change the displayed language of a rendered website. Localization support in .NET is nothing new. It&#8217;s possible with translation files, for example, <em>*.{locale}.resx<\/em>, <em>*.{locale}.xliff<\/em>, or <em>*.{locale}.restext<\/em> to name a few. The <code>CultureInfo<\/code> class is used along with these translation files and various other .NET employed mechanics. However, maintaining translation files can be tedious and time-consuming. With GitHub Actions and <a href=\"https:\/\/docs.microsoft.com\/azure\/cognitive-services\/translator?WT.mc_id=dapine\">Azure Cognitive Services Translator<\/a>, you can set up a workflow to automatically create pull requests that provide machine-translated files.<\/p>\n<h2>Azure Cognitive Services Translator<\/h2>\n<p>Cognitive Services Translator is a cloud-based machine translation service from Azure. It powers the GitHub Action, providing the root translation functionality. To use the action, you will need a <a href=\"https:\/\/docs.microsoft.com\/azure\/cognitive-services\/translator?WT.mc_id=dapine\">Cognitive Services Translator<\/a> resource. You can use an existing one, or <a href=\"https:\/\/ms.portal.azure.com\/#create\/Microsoft.CognitiveServicesTextTranslation\">create a new one<\/a>. If you do not have an Azure account, you can <a href=\"https:\/\/azure.microsoft.com\/free\/dotnet\">create one for free<\/a>. This resource is used to perform the translations from the GitHub Action through the <a href=\"https:\/\/docs.microsoft.com\/azure\/cognitive-services\/translator\/reference\/v3-0-reference?WT.mc_id=dapine\">Translator API v3<\/a>. In other words, as you <em>push<\/em> code changes to your GitHub repository that include <em>*.en.resx<\/em> files, this action runs when correctly specified in the workflow.<\/p>\n<p>For more information on filtering when actions run due to changes in specific files, see <a href=\"https:\/\/docs.github.com\/en\/free-pro-team@latest\/actions\/reference\/workflow-syntax-for-github-actions#onpushpull_requestpaths\">Workflow syntax for GitHub Actions<\/a>.<\/p>\n<h2>Machine Translator GitHub Action<\/h2>\n<p>The Machine Translator GitHub Action is available on the <a href=\"https:\/\/github.com\/marketplace\/actions\/machine-translator\">GitHub action marketplace<\/a>. This GitHub Action does the work of marrying the functionality of the Cognitive Services Translator with your source files. To use this action, you&#8217;ll need to <a href=\"#create-workflow\">create a GitHub workflow<\/a>. There are a few required inputs (and some optional), most of which are from your <a href=\"#azure-cognitive-services-translator\">Azure Translator resource<\/a>:<\/p>\n<table>\n<thead>\n<tr>\n<th align=\"right\">Type<\/th>\n<th align=\"left\">Input name<\/th>\n<th>Example \/ Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td align=\"right\">Required<\/td>\n<td align=\"left\"><code>sourceLocale<\/code><\/td>\n<td><code>'en'<\/code>\nThe source locale to translate from.<\/td>\n<\/tr>\n<tr>\n<td align=\"right\">Required<\/td>\n<td align=\"left\"><code>subscriptionKey<\/code><\/td>\n<td><code>'c571d5d8xxxxxxxxxxxxxxxxxx56bac3'<\/code>\nCognitive Services Translator subscription key, ideally stored as <a href=\"https:\/\/docs.github.com\/en\/free-pro-team@latest\/actions\/reference\/encrypted-secrets\">secret<\/a>.<\/td>\n<\/tr>\n<tr>\n<td align=\"right\">Required<\/td>\n<td align=\"left\"><code>endpoint<\/code><\/td>\n<td><code>'https:\/\/api.cognitive.microsofttranslator.com\/'<\/code>\nCognitive Services Translator endpoint, ideally stored as <a href=\"https:\/\/docs.github.com\/en\/free-pro-team@latest\/actions\/reference\/encrypted-secrets\">secret<\/a>.<\/td>\n<\/tr>\n<tr>\n<td align=\"right\">Optional<\/td>\n<td align=\"left\"><code>region<\/code><\/td>\n<td><code>'canadacentral'<\/code>\nCognitive Services Translator region, ideally stored as secret. Optional when using a global translator resource.<\/td>\n<\/tr>\n<tr>\n<td align=\"right\">Optional<\/td>\n<td align=\"left\"><code>toLocales<\/code><\/td>\n<td><code>'\"es,de,fr\"'<\/code> or <code>'[\"es\",\"de\",\"fr\"]'<\/code>\nLimit the scope of the translation targets. If <em>not<\/em> provided, uses all possible translation targets.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Additionally, the action requires a <code>GITHUB_TOKEN<\/code> as an environment variable. GitHub automatically creates a <code>GITHUB_TOKEN<\/code> secret to use in your workflow as an encrypted secret. To define this environment variable, use the following YAML within your workflow (more on this later):<\/p>\n<pre><code class=\"yml\">env:\r\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\r\n<\/code><\/pre>\n<p>For more information, see <a href=\"https:\/\/docs.github.com\/en\/free-pro-team@latest\/actions\/reference\/authentication-in-a-workflow#about-the-github_token-secret\">GitHub Actions: Authentication in a workflow\n<\/a>.<\/p>\n<h3>Machine Translator design<\/h3>\n<p>The Machine Translator action is entirely open source. It is written in <a href=\"https:\/\/github.com\/actions\/typescript-action\">TypeScript<\/a>, and designed to accomplish several key objectives:<\/p>\n<ul>\n<li>Determine which languages are available for translation from the translator API<\/li>\n<li>Read all translation files (<em>*.resx<\/em> for example) as source inputs for translation<\/li>\n<li>Translate all inputs into the available languages, or configured targets<\/li>\n<li>Create (or update existing) translation files<\/li>\n<\/ul>\n<p>To see how the action is actually implemented, feel free to view the source on the <a href=\"https:\/\/github.com\/IEvangelist\/resource-translator\">GitHub repository<\/a>.<\/p>\n<p>The scope of this action is limited to reading input translation files, translating new ones from these sources, and then writing the translation files back to the workspace. Part of the intended workflow composition is to pair this action with two others. The first is the <a href=\"https:\/\/github.com\/actions\/checkout\"><code>actions\/checkout@v2<\/code><\/a> action, which checks out your repository under the workspace so that the action can access it. The second is the <a href=\"https:\/\/github.com\/marketplace\/actions\/create-pull-request\"><code>peter-evans\/create-pull-request@v3.4.1<\/code><\/a> action, which will create a pull request if files are changed. Special shout-out to <a href=\"https:\/\/github.com\/peter-evans\">Peter Evans \ud83c\udf89<\/a> for his work here!<\/p>\n<h3>Open and active development<\/h3>\n<p>This action is actively being developed. It now has full support for the RESX, RESTEXT, and INI file formats, and basic support of XLIFF and PO file formats. XLIFF is another common industry-standard for resource management, while RESTEXT is a simpler INI-based key-value-pair alternative. The Machine Translator automatically handles batching of rate-limited API calls to Cognitive Services Translator. Many thanks to <a href=\"https:\/\/github.com\/IEvangelist\/resource-translator\/issues?q=is%3Aissue+author%3Atimheuer+\">Tim Heuer \ud83e\udd18\ud83c\udffc<\/a> for his collaboration and feedback! To propose ideas, feature requests, or post issues &#8211; please do so on the <a href=\"https:\/\/github.com\/IEvangelist\/resource-translator\/issues\">GitHub repository<\/a>.<\/p>\n<h2>&#8220;Blazing Translations&#8221; demo app<\/h2>\n<p>This GitHub Action is based on the notion of resource files and localization in .NET. Any .NET application that follows this localization paradigm is free to use this action. In this way, the action is <em>not<\/em> limited to just ASP.NET Core Blazor Wasm apps &#8211; that is just the demo app of choice for this post. The GitHub repository for the demo app is available at <a href=\"https:\/\/github.com\/IEvangelist\/IEvangelist.BlazingTranslations\">IEvangelist\/IEvangelist.BlazingTranslations<\/a>. The repository has several <strong>Secrets<\/strong>, which store encrypted values that can be accessed from a workflow. For more information, see <a href=\"https:\/\/docs.github.com\/en\/free-pro-team@latest\/actions\/reference\/encrypted-secrets\">GitHub Action reference: Encrypted Secrets<\/a>.<\/p>\n<p>The demo app was inspired by <a href=\"https:\/\/github.com\/pranavkm\/LocSample\">Pranav Krishnamoorthy&#8217;s LocSample<\/a> but adds a bit more exemplary source code. Some of the key components are:<\/p>\n<ul>\n<li>In the <em>Client<\/em> project, call the <code>IServiceCollection.AddLocalization()<\/code> extension method to register localization services<\/li>\n<li>Using dependency injection, inject <code>IStringLocalizer&lt;T&gt;<\/code> where string literals are used<\/li>\n<li>Add resource files that shadow Razor components and pages. For example, <em>Index.razor<\/em> should be accompanied by an <em>Index.en.resx<\/em> file<\/li>\n<li>Replace string literals with resource key-value pairs<\/li>\n<\/ul>\n<h3>Example Razor component<\/h3>\n<pre><code class=\"html\">@page \"\/\"\r\n\r\n&lt;h1&gt;@HelloWorld&lt;\/h1&gt;\r\n\r\n@Greeting\r\n\r\n&lt;SurveyPrompt Title=\"@SurveyTitle\" \/&gt;\r\n<\/code><\/pre>\n<p>In the preceding Razor markup, there are a few <code>@<\/code> directives that call into the code-behind, which simply access their property values. Consider the following <em>Index.razor.cs<\/em> file, which shadows the Razor component.<\/p>\n<pre><code class=\"csharp\">using Microsoft.AspNetCore.Components;\r\nusing Microsoft.Extensions.Localization;\r\n\r\nnamespace IEvangelist.BlazingTranslations.Client.Pages\r\n{\r\n    public partial class Index\r\n    {\r\n        [Inject]\r\n        public IStringLocalizer&lt;Index&gt; Localizer { get; set; }\r\n\r\n        public string SurveyTitle =&gt; Localizer[nameof(SurveyTitle)];\r\n        public string Greeting =&gt; Localizer[nameof(Greeting)];\r\n        public string HelloWorld =&gt; Localizer[nameof(HelloWorld)];\r\n    }\r\n}\r\n<\/code><\/pre>\n<p>The <code>partial class<\/code> is shared with the <code>Index<\/code> Razor component&#8217;s generated class. As such, this is thought of as the &#8220;code-behind&#8221;. It uses the <code>[Inject]<\/code> attribute to inject the <code>IStringLocalizer&lt;Index&gt;<\/code>. There are three readonly properties which are expressed as localizer indexer accessors. In this example, the corresponding <em>Index.en.resx<\/em> resource file is similar to the following:<\/p>\n<pre><code class=\"xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?&gt;\r\n&lt;root&gt;\r\n  &lt;data name=\"Greeting\" xml:space=\"preserve\"&gt;\r\n    &lt;value&gt;Welcome to your new app.&lt;\/value&gt;\r\n  &lt;\/data&gt;\r\n  &lt;data name=\"HelloWorld\" xml:space=\"preserve\"&gt;\r\n    &lt;value&gt;Hello, world!&lt;\/value&gt;\r\n  &lt;\/data&gt;\r\n  &lt;data name=\"SurveyTitle\" xml:space=\"preserve\"&gt;\r\n    &lt;value&gt;How is Blazor working for you?&lt;\/value&gt;\r\n  &lt;\/data&gt;\r\n&lt;\/root&gt;\r\n<\/code><\/pre>\n<p>Now that you have your resource file, and Razor components defined, you need to create the workflow.<\/p>\n<h3>Create workflow<\/h3>\n<p>Workflows are defined within the <em>.github\/workflows<\/em> directory from the root of the repository and are written as YAML files. Consider the following:<\/p>\n<pre><code class=\"yml\">name: Create translation pull request\r\non:\r\n  push:\r\n    branches: [ main ]\r\n    paths:\r\n    - '**.en.resx' # only take action when *.en.resx files change\r\n\r\nenv:\r\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Available by default, contextual to the action\r\n\r\njobs:\r\n  build:\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      # Checks-out repository under the workspace, so that the action can access it\r\n      - uses: actions\/checkout@v2\r\n\r\n      # Use the machine-translator to automatically translate resource files\r\n      - name: Machine Translator\r\n        id: translator\r\n        uses: IEvangelist\/resource-translator@v2.1.1\r\n        with:\r\n          subscriptionKey: ${{ secrets.AZURE_TRANSLATOR_SUBSCRIPTION_KEY }}\r\n          endpoint: ${{ secrets.AZURE_TRANSLATOR_ENDPOINT }}\r\n          region: ${{ secrets.AZURE_TRANSLATOR_REGION }}\r\n          sourceLocale: 'en'\r\n\r\n      # Creates a pull request of all translated resource files\r\n      - name: Create pull request\r\n        uses: peter-evans\/create-pull-request@v3.4.1\r\n        if: ${{ steps.resource-translator.outputs.has-new-translations }} == 'true'\r\n        with:\r\n          title: '${{ steps.resource-translator.outputs.summary-title }}'\r\n          body: '${{ steps.resource-translator.outputs.summary-details }}'\r\n<\/code><\/pre>\n<p>The preceding workflow definition will run when any <em>*.en.resx<\/em> file is either created or changed.<\/p>\n<h2>Putting it all together<\/h2>\n<p>With all the moving pieces in place, you the developer, are empowered to develop as you normally would. As you create and update Razor components and corresponding resource files, pull requests are automatically created for your review with translated resource files. Since pull requests are created, they can be updated by translation specialists if need be &#8211; but this will serve as a great starting point nonetheless.<\/p>\n<h3>Example pull request<\/h3>\n<p>Here is a link to an example automated <a href=\"https:\/\/github.com\/IEvangelist\/IEvangelist.BlazingTranslations\/pull\/20\">pull request<\/a>. The automated pull request was <a href=\"https:\/\/github.com\/IEvangelist\/IEvangelist.BlazingTranslations\/commit\/90f1990373f47a65ddc07ba43f0ce434e180ae11\">triggered by a commit<\/a> that simply updated several <em>*.en.resx<\/em> files. The pull request details a summary of translations.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2020\/12\/example-pr.png\" alt=\"GitHub pull request #20 from IEvangelist\/IEvangelist.BlazingTranslations\" \/><\/p>\n<h2>Summary<\/h2>\n<p>The Azure Cognitive Services Translator API serves as the backbone to the Machine Translator GitHub Action. With great power, comes great responsibility. As a developer, you must wear your ethics-hat at all times, especially when working with artificial intelligence (AI). It might seem as though this GitHub Action can easily elevate your .NET apps to be inclusive of nearly 80 languages, it is irresponsible to make such claims. These are machine translations and should be treated as such. Without direct intervention from a human review, the machine translations may not be conversationally accurate or culturally specific to what you are trying to convey in the text being translated.<\/p>\n<p>While I&#8217;m hopeful that you will use this action, I encourage you to also consider limiting the scope of translations to known locales with the <code>toLocales<\/code> input. Work closely with the stakeholders during the app development, and incrementally solicit feedback on translations from those who are consuming that app.<\/p>\n<h3>See also<\/h3>\n<ul>\n<li><a href=\"https:\/\/github.com\/IEvangelist\/resource-translator\">Machine Translator GitHub repo<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/marketplace\/actions\/machine-translator\">Machine Translator GitHub marketplace<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/IEvangelist\/IEvangelist.BlazingTranslations\">Blazing translations demo GitHub repo<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/timheuer\/simple-loc-winforms\">.NET 5 WinForms localization app repo<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/IEvangelist\/IEvangelist.BlazingTranslations\/runs\/1412742058\">Example successful automation run<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>A GitHub Action harnessing Azure Cognitive Services Translator to automatically create translation files.<\/p>\n","protected":false},"author":24662,"featured_media":31276,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,328,197,327,688],"tags":[4,7189,37,7222],"class_list":["post-31275","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-aiml","category-aspnet","category-azure","category-machine-learning","tag-net","tag-machinelearning","tag-azure","tag-github"],"acf":[],"blog_post_summary":"<p>A GitHub Action harnessing Azure Cognitive Services Translator to automatically create translation files.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/31275","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/24662"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=31275"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/31275\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/31276"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=31275"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=31275"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=31275"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}