Localization Resource Generator & Translator via "dotnet" CLI

Oct 17, 2016     Viewed 5007 times    2 Comments
Posted in #Localization  #CLI 

ِEvery web developer knows that the globalization & localization are very common in almost the web applications, ASP.NET Core gives us a rich set of APIs that make the entire localization process easier than before.

However you need to add a resource files (resx) in order to accpomplish the process, personally I - and may be you - found this tedios, because you need to add many resource files per culture for views, controllers .. etc, and not only that!! send them to a transaltor or use an online service.

So I was thinking few days back to create a tool using dotnet CLI to generate and translate the localization resource files to accelarate the process. It is a simple tool but cool and useful, hope I find time to improve it. The basic idea is automate the creation of the resource files and using Microsoft Translation APIs in order to save the time of translation, and make the process fast in simple go.

Now let us dive little bit in resgen command, the tool seems like Microsoft Resgen.exe in some functionalities, but it's more than that. As we know ASP.NET Core shifts from using DNX to using a new set of commands called dotnet CLI which is an open source and hosted in GitHub.

I started by visiting this link which gave me an idea of how you can create your own custom dotnet CLI command, which gave me some glimpse to how to start.

dotnet-resgen

dotnet-resgen is a localization resource (resx) generator for the .NET Core applications. Also it translates the resource entries using Microsoft Translation APIs during the generation process.

Installation

Simply add the binaries folder - which is located in LocalizationResourceGenerator\src\LocalizationResourceGenerator\bin\Debug\netcoreapp1.0\win10-x64 - to the System PATH.

Usage

dotnet resgen [arguments] [options]

Arguments:
  cultures   List of cultures, that the 'dotnet-resgen' command will generate a resource file(s) for each one for them

Options:
  -h|--help            Show help information
  --default <CULTURE>  The default culture that the 'dotnet-resgen' command will use it to translate from
  -t|--type <TYPE>     The type of the resource file [resx|restext]

There are few steps you should consider before using the dotnet resgen command:

  • Create your default resource file without append the culture name in your resource directory
  • Go to the resource directory using the command line
  • Run dotnet resgen command

Let us see some code to make this magic in reality:

private static ILoggerFactory _loggerFactory = new LoggerFactory();
private static ILogger _logger;
private static readonly string _appKey = "6CE9C85A41571C050C379F60DA173D286384E0F2";
private static readonly string _defaultCulture = "en";
private static readonly string _defaultType = "resx";
private static readonly LanguageServiceClient client = new LanguageServiceClient(EndpointConfiguration.BasicHttpBinding_LanguageService);

public static void Main(string[] args)
{
    var app = new CommandLineApplication();

    app.Name = "dotnet resgen";
    app.FullName = "Resource Generator";
    app.Description = "Generates and translates a localization resource file(s)";
    app.HandleResponseFiles = true;
    app.HelpOption("-h|--help");

    var culturesCommand = app.Argument("cultures", "List of cultures, that the 'dotnet-resgen' command will generate a resource file(s) for each one for them", true);
    var defaultCultureOption = app.Option("--default ", "The default culture that the 'dotnet-resgen' command will use it to translate from", CommandOptionType.SingleValue);
    var resourceTypeOption = app.Option("-t|--type ", "The type of the resource file [resx|restext]", CommandOptionType.SingleValue);
    var resourceLocationOption = app.Option("-l|--location ", "The location of the resources directory", CommandOptionType.SingleValue);

    _loggerFactory.AddConsole();
    _logger = _loggerFactory.CreateLogger(nameof(Program));

    app.OnExecute(async () =>
    {
        var defaultCulture = defaultCultureOption.Value() ?? _defaultCulture;
        var cultures = culturesCommand.Values;
        var resourceType = resourceTypeOption.Value() ?? _defaultType;
        var currentDirectory = Directory.GetCurrentDirectory();
        var resourceLocation = Path.Combine(currentDirectory, resourceLocationOption.Value());

        if (culturesCommand.Values.Count == 0)
        {
            _logger.LogError("The argument named 'cultures' is required in order to generate the resource files.");
            return 1;
        }

        switch (resourceType)
        {
            case "resx":
            case "restext":
                var resourceExtension = resourceType;
                XDocument doc = null;

                foreach (var filePath in Directory.GetFiles(currentDirectory, "*." + resourceExtension))
                {
                    var file = new FileInfo(filePath);

                    if (!Path.GetFileNameWithoutExtension(file.Name).Contains("."))
                    {
                        foreach (var culture in cultures)
                        {
                            var resourceFileName = string.Join(".", Path.GetFileNameWithoutExtension(file.Name), culture, resourceExtension);
                            var resourcePath = Path.Combine(currentDirectory, resourceFileName);

                            if (resourceExtension == "resx")
                            {
                                doc = XDocument.Load(resourcePath);
                                File.Copy(file.FullName, resourcePath, true);
                            }
                            else
                            {
                                doc = ResourceTextFile.Load(file.OpenRead());
                            }

                            foreach (var element in doc.Root.Elements("data"))
                            {
                                var key = element.Element("value").Value;
                                var result = await client.TranslateAsync(_appKey, key.ToString(), defaultCulture, culture);

                                element.SetElementValue("value", result);
                            }

                            using (var stream = new FileStream(resourcePath, FileMode.Open, FileAccess.Write))
                            {
                                doc.Save(stream);
                            }
                        }
                    }
                }

                return 0;
            default:
                return 1;
        }
    });

    app.Execute(args);
}

The tool rely heavly on CommandLine helpers which I use them from Microsoft.DotNet.Cli.CommandLine for command parsing, instead of rewrite everything from scratch. Basic I added some command arguments and options based on assumption which illustrated in the Usage section above, and the code simply loop through the resources directory, discover the default resource files, after that it generates a new resources per culture, meanwhile it call a Microsoft Translation APIs to translate the resource keys to the desired culture.

The current implementation generate localization resource files from a default resx files as well as restext files, which is basically a text-based file has INI syntax to simplify resource entries representation, instead of wired XML content that we have seen in any resx file.

You can download the source code for this post from my LocalizationResourceGenerator repository.

Twitter Facebook Google + LinkedIn


2 Comments

Mindy (3/8/2017 7:03:49 AM)

An good tool to localize .resx files is htttps://poeditor.com which can be used to translate strings into any language in a team.
I find it really boosts productivity and makes localization management a lot easier.

jahan (4/27/2017 2:31:56 AM)

nice website


Leave a Comment