Import, Export ASP.NET Core localized data as CSV

This article shows how localized data can be imported and exported using Localization.SqlLocalizer. The data is exported as CSV using the Formatter defined in the WebApiContrib.Core.Formatter.Csv package. The data can be imported using a file upload.

This makes it possible to export the applications localized data to a CSV format. A translation company can then translate the data, and it can be imported back into the application.

Code: https://github.com/damienbod/AspNet5Localization

The two required packages are added to the project.json file. The Localization.SqlLocalizer package is used for the ASP.NET Core localization. The WebApiContrib.Core.Formatter.Csv package defines the CSV InputFormatter and OutputFormatter.

"Localization.SqlLocalizer": "1.0.5",
"WebApiContrib.Core.Formatter.Csv": "1.0.1"

The packages are then added in the Startup class. The DBContext LocalizationModelContext is added and also the ASP.NET Core localization middleware. The InputFormatter and the OutputFormatter are alos added to the MVC service.

using System;
using System.Collections.Generic;
using System.Globalization;
using Localization.SqlLocalizer.DbStringLocalizer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using WebApiContrib.Core.Formatter.Csv;

namespace ImportExportLocalization
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            var sqlConnectionString = Configuration["DbStringLocalizer:ConnectionString"];

            services.AddDbContext<LocalizationModelContext>(options =>
                options.UseSqlite(
                    sqlConnectionString,
                    b => b.MigrationsAssembly("ImportExportLocalization")
                )
            );

            // Requires that LocalizationModelContext is defined
            services.AddSqlLocalization(options => options.UseTypeFullNames = true);

            services.AddMvc()
                .AddViewLocalization()
                .AddDataAnnotationsLocalization();

            services.Configure<RequestLocalizationOptions>(
                options =>
                {
                    var supportedCultures = new List<CultureInfo>
                        {
                            new CultureInfo("en-US"),
                            new CultureInfo("de-CH"),
                            new CultureInfo("fr-CH"),
                            new CultureInfo("it-CH")
                        };

                    options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
                    options.SupportedCultures = supportedCultures;
                    options.SupportedUICultures = supportedCultures;
                });

            var csvFormatterOptions = new CsvFormatterOptions();

            services.AddMvc(options =>
            {
                options.InputFormatters.Add(new CsvInputFormatter(csvFormatterOptions));
                options.OutputFormatters.Add(new CsvOutputFormatter(csvFormatterOptions));
                options.FormatterMappings.SetMediaTypeMappingForFormat("csv", MediaTypeHeaderValue.Parse("text/csv"));
            });

            services.AddScoped<ValidateMimeMultipartContentFilter>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
            app.UseRequestLocalization(locOptions.Value);

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

The ImportExportController makes it possible to download all the localized data as a csv file. This is implemented in the GetDataAsCsv method. This file can then be emailed or whatever to a translation company. When the updated file is returned, it can be imported using the ImportCsvFileForExistingData method. The method accepts the file and updates the data in the database. It is also possible to add new csv data, but care has to be taken as the key has to match the configuration of the Localization.SqlLocalizer middleware.

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Localization.SqlLocalizer.DbStringLocalizer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;

namespace ImportExportLocalization.Controllers
{
    [Route("api/ImportExport")]
    public class ImportExportController : Controller
    {
        private IStringExtendedLocalizerFactory _stringExtendedLocalizerFactory;

        public ImportExportController(IStringExtendedLocalizerFactory stringExtendedLocalizerFactory)
        {
            _stringExtendedLocalizerFactory = stringExtendedLocalizerFactory;
        }

        // http://localhost:6062/api/ImportExport/localizedData.csv
        [HttpGet]
        [Route("localizedData.csv")]
        [Produces("text/csv")]
        public IActionResult GetDataAsCsv()
        {
            return Ok(_stringExtendedLocalizerFactory.GetLocalizationData());
        }

        [Route("update")]
        [HttpPost]
        [ServiceFilter(typeof(ValidateMimeMultipartContentFilter))]
        public IActionResult ImportCsvFileForExistingData(CsvImportDescription csvImportDescription)
        {
            // TODO validate that data is a csv file.
            var contentTypes = new List<string>();

            if (ModelState.IsValid)
            {
                foreach (var file in csvImportDescription.File)
                {
                    if (file.Length > 0)
                    {
                        var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"');
                        contentTypes.Add(file.ContentType);

                        var inputStream = file.OpenReadStream();
                        var items = readStream(file.OpenReadStream());
                        _stringExtendedLocalizerFactory.UpdatetLocalizationData(items, csvImportDescription.Information);
                    }
                }
            }

            return RedirectToAction("Index", "Home");
        }

        [Route("new")]
        [HttpPost]
        [ServiceFilter(typeof(ValidateMimeMultipartContentFilter))]
        public IActionResult ImportCsvFileForNewData(CsvImportDescription csvImportDescription)
        {
            // TODO validate that data is a csv file.
            var contentTypes = new List<string>();

            if (ModelState.IsValid)
            {
                foreach (var file in csvImportDescription.File)
                {
                    if (file.Length > 0)
                    {
                        var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"');
                        contentTypes.Add(file.ContentType);

                        var inputStream = file.OpenReadStream();
                        var items = readStream(file.OpenReadStream());
                        _stringExtendedLocalizerFactory.AddNewLocalizationData(items, csvImportDescription.Information);
                    }
                }
            }

            return RedirectToAction("Index", "Home");
        }

        private List<LocalizationRecord> readStream(Stream stream)
        {
            bool skipFirstLine = true;
            string csvDelimiter = ";";

            List<LocalizationRecord> list = new List<LocalizationRecord>();
            var reader = new StreamReader(stream);


            while (!reader.EndOfStream)
            {
                var line = reader.ReadLine();
                var values = line.Split(csvDelimiter.ToCharArray());
                if (skipFirstLine)
                {
                    skipFirstLine = false;
                }
                else
                {
                    var itemTypeInGeneric = list.GetType().GetTypeInfo().GenericTypeArguments[0];
                    var item = new LocalizationRecord();
                    var properties = item.GetType().GetProperties();
                    for (int i = 0; i < values.Length; i++)
                    {
                        properties[i].SetValue(item, Convert.ChangeType(values[i], properties[i].PropertyType), null);
                    }

                    list.Add(item);
                }

            }

            return list;
        }
    }
}

The index razor view has a download link and also 2 upload buttons to manage the localization data.


<fieldset>
    <legend style="padding-top: 10px; padding-bottom: 10px;">Download existing translations</legend>

    <a href="http://localhost:6062/api/ImportExport/localizedData.csv" target="_blank">localizedData.csv</a>

</fieldset>

<hr />

<div>
    <form enctype="multipart/form-data" method="post" action="http://localhost:6062/api/ImportExport/update" id="ajaxUploadForm" novalidate="novalidate">
        <fieldset>
            <legend style="padding-top: 10px; padding-bottom: 10px;">Upload existing CSV data</legend>

            <div class="col-xs-12" style="padding: 10px;">
                <div class="col-xs-4">
                    <label>Upload Information</label>
                </div>
                <div class="col-xs-7">
                    <textarea rows="2" placeholder="Information" class="form-control" name="information" id="information"></textarea>
                </div>
            </div>

            <div class="col-xs-12" style="padding: 10px;">
                <div class="col-xs-4">
                    <label>Upload CSV data</label>
                </div>
                <div class="col-xs-7">
                    <input type="file" name="file" id="fileInput">
                </div>
            </div>

            <div class="col-xs-12" style="padding: 10px;">
                <div class="col-xs-4">
                    <input type="submit" value="Upload Updated Data" id="ajaxUploadButton" class="btn">
                </div>
                <div class="col-xs-7">

                </div>
            </div>

        </fieldset>
    </form>
</div>

<div>
    <form enctype="multipart/form-data" method="post" action="http://localhost:6062/api/ImportExport/new" id="ajaxUploadForm" novalidate="novalidate">
        <fieldset>
            <legend style="padding-top: 10px; padding-bottom: 10px;">Upload new CSV data</legend>

            <div class="col-xs-12" style="padding: 10px;">
                <div class="col-xs-4">
                    <label>Upload Information</label>
                </div>
                <div class="col-xs-7">
                    <textarea rows="2" placeholder="Information" class="form-control" name="information" id="information"></textarea>
                </div>
            </div>

            <div class="col-xs-12" style="padding: 10px;">
                <div class="col-xs-4">
                    <label>Upload CSV data</label>
                </div>
                <div class="col-xs-7">
                    <input type="file" name="file" id="fileInput">
                </div>
            </div>

            <div class="col-xs-12" style="padding: 10px;">
                <div class="col-xs-4">
                    <input type="submit" value="Upload New Data" id="ajaxUploadButton" class="btn">
                </div>
                <div class="col-xs-7">

                </div>
            </div>

        </fieldset>
    </form>
</div>

The data can then be managed as required.

localizedDataCsvImportExport_01

The IStringExtendedLocalizerFactory offers all the import export functionality which is supported by Localization.SqlLocalizer. If anything else is required, please create an issue or use the source code and extend it yourself.

public interface IStringExtendedLocalizerFactory : IStringLocalizerFactory
{
	void ResetCache();

	void ResetCache(Type resourceSource);

	IList GetImportHistory();

	IList GetExportHistory();

	IList GetLocalizationData(string reason = "export");

	IList GetLocalizationData(DateTime from, string culture = null, string reason = "export");

	void UpdatetLocalizationData(List<LocalizationRecord> data, string information);

	void AddNewLocalizationData(List<LocalizationRecord> data, string information);
}

Links:

https://www.nuget.org/packages/Localization.SqlLocalizer/

https://www.nuget.org/packages/WebApiContrib.Core.Formatter.Csv/

One comment

  1. […] Importer des données localisées en CSV avec ASP.NET Core. […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: