No artigo de introdução, explicamos um pouco dos conceitos de internacionalização de uma aplicação, bem como alguns dos termos mais importantes. Neste artigo iremos começar o desenvolvimento de nossa aplicação globalizada e já localizá-la para dois idiomas. Faremos a implementação padrão disponibilizada pelo framework e, em seguida, customizaremos um pouco essa implementação para organizarmos melhor nossos arquivos de localização.

Mãos à obra!

Criando a aplicação

Primeiramente, vamos criar um novo projeto ASP.NET Core abrindo o Visual Studio e selecionando “Create new project”. Na janela de seleção de template de projeto, busque por “asp”, selecione “ASP.NET Core Web Application” e, em seguida, clique em “Next”.

Novo projeto.
Novo projeto

Na próxima tela, informe o nome do projeto, local que será salvo, o nome da Solution e clique em create.

Nome do projeto.
Nome do projeto

Na próxima tela, selecione o “Web Application (Model-View-Controller)” e clique em create.

Tipo de projeto Web Application.
Tipo de projeto Web Application

Após término da criação do projeto, ao executá-lo, será exibida a seguinte tela:

Tela inicial do projeto.
Tela inicial do projeto

Com tudo funcionando, podemos começar o processo de globalizar nossa aplicação.

Globalização

O ASP.NET Core utiliza as interfaces IStringLocalizerIStringLocalizer <T> para auxiliar no desenvolvimento de aplicações globalizadas. Elas utilizam as classes ResourceManager e ResourceReader para prover os textos traduzidos no idioma selecionado em tempo de execução. É possível segmentar os arquivos de tradução por Controller, Area ou ter tudo em um arquivo só (nossa opção).

Vamos começar criando uma classe “dummy” chamada SharedResource na raiz de nosso projeto. Ela será o nosso “indexador” de traduções.

namespace Globalization
{
    public class SharedResource
    {
    }
}

Com a classe criada, iniciaremos com um exemplo de como recuperar textos pelo Controller.

Abra o arquivo Controllers/HomeController.cs e implemente o código:

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Globalization.Models;
using Microsoft.Extensions.Localization;

namespace Globalization.Controllers
{
    public class HomeController : Controller
    {
        private readonly IStringLocalizer _localizer;

        public HomeController(IStringLocalizer<SharedResource> localizer)
        {
            _localizer = localizer;
        }

        public IActionResult Index()
        {
            ViewBag.PageTitle = _localizer["Index_Page_Title"];
            return View();
        }

        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

Aqui, criamos um construtor que receberá uma instância de IStringLocalizer<SharedResource> via Dependency Injection (que será configurado mais à frente) e, ao acessar a Action “Index”, atribuímos à ViewBag “PageTitle” o texto traduzido de chave “Index_Page_Title”. Vamos utilizar esta ViewBag para setar o título da página no idioma selecionado pelo usuário, implementando o seguinte trecho no arquivo Views/Home/Index.cshtml:

@{
    ViewData["Title"] = ViewBag.PageTitle;
}

Agora, vamos criar um exemplo recuperando um texto pela View.

Abra o arquivo Views/_ViewImports.cshtml e adicione a linha “@using Microsoft.Extensions.Localization” à lista, ficando da seguinte maneira:

@using Globalization
@using Globalization.Models
@using Microsoft.Extensions.Localization
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Adicionamos o import do namespace neste arquivo para tirar a necessidade de adicionar esta mesma linha em todos os arquivos que necessitarem acesso aos textos de tradução. Em aplicações maiores, isto ajuda bastante a evitar replicação de código.

Depois, abriremos novamente ao arquivo Views/Home/Index.cshtml e iremos transformar o título principal da página “Welcome” em um texto “globalizado”. O arquivo deve ficar da seguinte maneira:

@{
    ViewData["Title"] = ViewBag.PageTitle;
}

@inject IStringLocalizer<SharedResource> Localizer

<div class="text-center">
    <h1 class="display-4">@Localizer["Welcome_Title"]</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

A ideia é a mesma da implementação do Controller. Recebemos a instância de IStringLocalizer<SharedResource> via DI e recuperamos o texto informado na chave “Welcome_Title”.

Nosso próximo passo será configurar o DI para retornar a instância de IStringLocalizer quando solicitado. Isto é feito adicionando a linha “services.AddLocalization()” no método “ConfigureServices” do arquivo Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization();

    services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services
        .AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Se tentarmos executar nossa aplicação novamente, veremos a seguinte tela:

Tela inicial após globalização configurada.
Tela inicial após globalização configurada

Note os textos da aba do navegador e do título principal da página. Eles são as chaves que utilizamos para recuperar um texto dos arquivos de tradução. Como ainda não criamos nenhum arquivo, o comportamento padrão do IStringLocalizer é retornar a chave informada.

Antes de localizarmos nossa aplicação, precisamos disponibilizar uma maneira da aplicação identificar qual o idioma que o usuário deseja utilizar. Para isso, utilizamos o middleware de globalização do ASP.NET, que dispõe dos seguintes providers para resolver o idioma da aplicação:

  • QueryStringRequestCultureProvider: idioma será informado via querystring (bom para debug);
  • CookieRequestCultureProvider: idioma será lido de um cookie;
  • AcceptLanguageHeaderRequestCultureProvider: idioma será identificado pelo header Accept-Language configurado do browser;
  • CustomRequestCultureProvider: implementação customizada pelo desenvolvedor.

Para facilitar nosso exemplo, utilizaremos o QueryStringRequestCultureProvider, informando o idioma desejado adicionando a chave culture à querystring da aplicação (Ex. http://localhost/?culture=pt-BR). O middleware deve ser configurado no método Configure do arquivo Startup.cs, antes de qualquer outro middleware que possa necessitar do idioma selecionado (por exemplo, app.UseMvc()).

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    //App supported cultures
    var supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("pt-BR"),
    };

    app.UseRequestLocalization(new RequestLocalizationOptions
    {
        DefaultRequestCulture = new RequestCulture("en-US"),
        // Formatting numbers, dates, etc.
        SupportedCultures = supportedCultures,
        // UI strings that we have localized.
        SupportedUICultures = supportedCultures,
        RequestCultureProviders = new List<IRequestCultureProvider> { new QueryStringRequestCultureProvider() }
    });

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();

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

No trecho anterior, configuramos o middleware com a chamada para app.UseRequestLocalization e passando como parâmetro qual o idioma padrão da aplicação, os idiomas suportados e o/os providers desejados.

Estamos quase lá…

Localização

Com nossa aplicação devidamente globalizada (por enquanto), só nos falta localizá-la criando os arquivos de tradução. Faremos isso criando um novo arquivo do tipo Resource (.resx), com o formato [nome].[idioma].resx. O [nome] é definido pelo nome completo do tipo do resource, menos o nome do assembly. No nosso exemplo, o nome assembly é Globalization e o nome completo do tipo do resource é Globalization.SharedResource (lembram de nosso “indexador”?), portanto, ele será somente SharedResource. Caso tivéssemos criado nosso tipo dentro do namespace Globalization.Resource.SharedResource, o [nome] deveria ser Resource.SharedResource. Já o [idioma], deve ser código do idioma do arquivo, por exemplo pt-BR, en-US, etc.

Dito isso, vamos criar o arquivo SharedResource.en-US.resx para colocar nossos textos em inglês:

Arquivo Resource en-US.
Arquivo Resource en-US

Neste arquivo, a coluna Name corresponde às chaves utilizadas para recuperar os textos e a coluna Value o texto no idioma correspondente. Com a versão em inglês finalizada, vamos criar a versão português com o arquivo SharedResource.pt-BR.resx.

Arquivo Resource pt-BR.
Arquivo Resource pt-BR

Com os arquivos de tradução criados, nós devemos ter nossa aplicação multi-idioma funcional. Vamos testar:

Aplicação utilizando Resource em inglês.
Aplicação utilizando Resource em inglês

Vemos que os textos da aba e do título principal estão em en-US, o idioma que definimos como o padrão da aplicação. Vamos tentar acessar a versão em português:

Aplicação utilizando Resource em português.
Aplicação utilizando Resource em português

Funcionou! A aplicação foi capaz de ler o resource do idioma que solicitamos corretamente!

Agora, imaginem se tivermos uma aplicação com mais idiomas suportados, 5 por exemplo. Para cada novo termo, nós precisamos abrir 5 arquivos diferentes para informar a tradução e, quanto mais termos tivermos, mais difícil será verificar se esquecemos de informar um ou mais termos em algum dos arquivos de tradução. Visto este problema, não seria mais simples se todos textos, de todos os idiomas, estivessem no mesmo arquivo?

Vamos ver isso na prática!

Customizando a Localização

Vamos criar uma nova pasta na raiz de nosso projeto e chamaremos de Resources. Nela, vamos adicionar todos os arquivos necessários para a nossa customização da localização. Começaremos criando nosso arquivo de traduções no formato JSON com o nome de Resource.json, ele conterá a lista de todos os termos utilizados pela aplicação e para cada um destes termos, teremos as traduções para cada idioma disponível. O arquivo ficará assim:

{
  "Index_Page_Title": {
    "en-US": "Initial Page - JSON",
    "pt-BR": "Página Inicial - JSON"
  },
  "Welcome_Title": {
    "en-US": "Welcome - JSON",
    "pt-BR": "Bem-vindo - JSON"
  }
}

Antes de criar o próximo arquivo, precisamos alterar as propriedades de nosso JSON (clique com o botão direito do mouse no arquivo e selecione Properties) e altere a opção Copy to Output Directory para Copy if newer, conforme imagem abaixo.

Propriedades do Resource JSON.
Propriedades do Resource JSON

Para ler este nosso novo arquivo de Resource, precisamos criar uma classe que implemente a interface IStringLocalizer. Criaremos uma nova classe em nosso projeto chamada JsonStringLocalizer. A implementação da classe será a seguinte:

using Microsoft.Extensions.Localization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;

namespace Globalization.Resources
{
    public class JsonStringLocalizer : IStringLocalizer
    {
        private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _resource =
            new ConcurrentDictionary<string, ConcurrentDictionary<string, string>>();

        public JsonStringLocalizer()
        {
            string path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Resources/Resource.json");
            using (StreamReader reader = File.OpenText(path))
            {
                var jObject = (JObject)JToken.ReadFrom(new JsonTextReader(reader));
                _resource = JsonConvert.DeserializeObject<ConcurrentDictionary<string, ConcurrentDictionary<string, string>>>(jObject.ToString());
            }
        }

        public LocalizedString this[string name] => GetStringResource(name);

        public LocalizedString this[string name, params object[] arguments] => GetStringResource(name, arguments);

        public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
        {
            return _resource.Keys.Select(r => new LocalizedString(r, r));
        }

        public IStringLocalizer WithCulture(CultureInfo culture)
        {
            return this;
        }

        private LocalizedString GetStringResource(string name, params object[] arguments)
        {
            if (string.IsNullOrWhiteSpace(name)
                || !_resource.TryGetValue(name, out ConcurrentDictionary<string, string> stringByCulture)
                || !stringByCulture.TryGetValue(CultureInfo.CurrentCulture.Name, out string value))
                return new LocalizedString(name, name, true);

            return new LocalizedString(name, string.Format(value, arguments), false);
        }
    }
}

Em seguida, precisamos alterar a configuração de DI da aplicação para utilizar a nossa nova implementação da interface. Para isto, criaremos uma nova classe chamada JsonLocalizerExtensions, com o código a seguir:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;

namespace Globalization.Resources
{
    public static class JsonLocalizerExtensions
    {
        public static void AddJsonLocalization(this IServiceCollection services)
        {
            services.AddSingleton<IStringLocalizer, JsonStringLocalizer>();
        }
    }
}

Esta classe contém uma extenção que adiciona nossa implementação da interface IStringLocalizer no DI. Para utilizá-la, devemos alterar a linha services.AddLocalization() do método ConfigureServices da classe Startup para services.AddJsonLocalization() e tanto no nosso HomeController quanto na view Index.cshtml, trocamos a injeção da interface IStringLocalizer<SharedResource> para somente IStringLocalizer.

Pronto! Nossa implementação customizada está pronta e configurada. Vamos ao teste:

Aplicação utilizando resource JSON em inglês.
Aplicação utilizando resource JSON em inglês

Até aqui, tudo bem. Vamos ver se conseguimos mudar o idioma:

Aplicação utilizando resource JSON em português.
Aplicação utilizando resource JSON em português

Tudo certo! Com somente um arquivo de resource contendo todos os termos e traduções, conseguimos replicar o funcionamento da implementação nativa de localização do ASP.NET, facilitando bastante o trabalho de manutenção das traduções.

No próximo artigo da série, customizaremos um pouco mais a aplicação e criaremos um novo CultureProvider. Fique ligado no blog da Iteris!


Fonte pode ser encontrado no GitHub: https://github.com/sandrocaseiro/asp-net-globalization.