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/AspNetCoreLocalization
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": "2.0.0", "WebApiContrib.Core.Formatter.Csv": "2.0.0"
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.
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/
[…] Importer des données localisées en CSV avec ASP.NET Core. […]
Would you kindly give an example of what to insert into the database. I keep getting null value errors when trying to retrieve anything from the api.
HI ovisariesdk
I’ll do an update on this, the migrations has changed.
Greetings Damien
Thanks Damien
This is the second time I’m trying to struggle with this sql localization project of your. And I must say that I’m just not able to make the project work, furthermore I don’t find the tutorial easy to understand neither the code.
It would be nice if you could expand your sql localization project with a simple page (view page) with a drop down list to select the language, and with pre-filled table values, just to get us started using your project.
I hope it makes sense.