Localização no .NET

Localização é o processo de traduzir os recursos do aplicativo em versões localizadas para cada cultura à qual o aplicativo dará suporte. Você deve prosseguir para a etapa de localização somente após concluir a etapa de Revisão de capacidade de localização para verificar se o aplicativo globalizado está pronto para a localização.

Um aplicativo que está pronto para a localização é separado em dois blocos conceituais: um bloco que contém todos os elementos de interface do usuário e um bloco que contém o código executável. O bloco de interface do usuário contém apenas os elementos de interface de usuário localizáveis, como cadeias de caracteres, mensagens de erro, caixas de diálogo, menus, recursos de objetos inseridos e assim por diante para a cultura neutra. O bloco de código contém apenas o código do aplicativo a ser usado por todas as culturas com suporte. O common language runtime dá suporte a um modelo de recurso de assembly satélite que separa o código executável do aplicativo de seus recursos. Para obter mais informações de como implementar esse modelo, confira Recursos no .NET.

Para cada versão localizada do aplicativo, adicione um novo assembly satélite que contenha o bloco de interface do usuário localizada traduzido para o idioma apropriado para a cultura de destino. O bloco de código para todas as culturas deve permanecer o mesmo. A combinação de uma versão localizada do bloco de interface de usuário com o bloco de código produz uma versão localizada do aplicativo.

Neste artigo, você aprenderá a usar as implementações do IStringLocalizer<T> e do IStringLocalizerFactory. Todo o código-fonte de exemplo neste artigo depende dos pacotes do NuGet Microsoft.Extensions.Localization e Microsoft.Extensions.Hosting. Para obter mais informações sobre hospedagem, confiraHost Genérico do .NET.

Arquivos de recurso

O principal mecanismo para isolar cadeias de caracteres localizáveis é usar arquivos de recurso. Um arquivo de recurso é um arquivo XML com a extensão de arquivo .resx. Os arquivos de recurso são convertidos antes da execução do aplicativo de consumo. Em outras palavras, eles representam o conteúdo convertido em repouso. O nome de um arquivo de recurso geralmente contém um identificador de localidade e tem a seguinte forma:

<FullTypeName><.Locale>.resx

Em que:

  • O <FullTypeName> representa os recursos localizáveis de um tipo específico.
  • O <.Locale> opcional representa a localidade do conteúdo do arquivo de recurso.

Especificação de localidades

A localidade deve definir o idioma, no mínimo, mas também pode definir a cultura (idioma regional) e até mesmo o país ou a região. Esses segmentos geralmente são delimitados pelo caractere -. Com a especificidade agregada de uma cultura, as regras de "fallback de cultura" são aplicadas onde as melhores correspondências são priorizadas. A localidade deve ser mapeada para uma marca de idioma bem conhecida. Para obter mais informações, consulte CultureInfo.Name.

Cenários de fallback de cultura

Imagine que o aplicativo localizado sejam compatível com várias localidades sérvias e tenha os seguintes arquivos de recurso para o MessageService:

Arquivo Idioma regional Código do país
MessageService.sr-Cyrl-RS.resx (Cirílico, Sérvia) RS
MessageService.sr-Cyrl.resx Cirílico
MessageService.sr-Latn-BA.resx (Latim, Bósnia e Herzegovina) BA
MessageService.sr-Latn-ME.resx (Latim, Montenegro) ME
MessageService.sr-Latn-RS.resx (Latim, Sérvia) RS
MessageService.sr-Latn.resx Latim
MessageService.sr.resx Latim
MessageService.resx

O idioma regional padrão do idioma.

Quando o aplicativo está em execução com o CultureInfo.CurrentCulture definido como cultura de "sr-Cyrl-RS", a localização tenta resolver os arquivos na seguinte ordem:

  1. MessageService.sr-Cyrl-RS.resx
  2. MessageService.sr-Cyrl.resx
  3. MessageService.sr.resx
  4. MessageService.resx

No entanto, se o aplicativo estiver em execução com o CultureInfo.CurrentCulture definido como cultura de "sr-Latn-BA", a localização tentará resolver os arquivos na seguinte ordem:

  1. MessageService.sr-Latn-BA.resx
  2. MessageService.sr-Latn.resx
  3. MessageService.sr.resx
  4. MessageService.resx

A regra de "fallback de cultura" ignorará as localidades quando não houver as respectivas correspondências, o que significa que o arquivo de recurso número quatro será selecionado se não for possível encontrar uma correspondência. Se a cultura foi definida como "fr-FR", a localização acaba resolvendo o arquivo MessageService.resx, o que pode ser problemático. Para obter mais informações, confira O processo de fallback do recurso.

Pesquisa de recursos

Os arquivos de recurso são resolvidos automaticamente como parte de uma rotina de pesquisa. Se o nome do arquivo de projeto for diferente do namespace raiz do projeto, o nome do assembly poderá ser diferente. Isso pode impedir que a pesquisa de recursos seja bem-sucedida. Para resolver essa incompatibilidade, use o RootNamespaceAttribute para fornecer uma dica aos serviços de localização. Quando fornecido, ele é usado durante a pesquisa de recursos.

O projeto de exemplo é chamado de example.csproj, o que cria um example.dll e um example.exe. No entanto, o namespace Localization.Example é usado. Aplique um atributo de nível assembly para corrigir essa incompatibilidade:

[assembly: RootNamespace("Localization.Example")]

Registrar os serviços de localização

Para registrar os serviços de localização, chame um dos métodos de extensão AddLocalization durante a configuração dos serviços. Essa ação habilitará a DI (injeção de dependência) dos seguintes tipos:

Configurar as opções de localização

A sobrecarga AddLocalization(IServiceCollection, Action<LocalizationOptions>) aceita um parâmetro setupAction do tipo Action<LocalizationOptions>. Isso permite que você configure as opções de localização.

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddLocalization(options =>
{
    options.ResourcesPath = "Resources";
});

// Omitted for brevity.

Os arquivos de recurso podem residir em qualquer lugar em um projeto, mas existem práticas comuns estabelecidas que provaram ser bem-sucedidas. Na maioria das vezes, o caminho de menor resistência é seguido. O código anterior do C#:

  • Cria o construtor do aplicativo de host padrão.
  • Chama AddLocalization na coleção de serviços, especificando LocalizationOptions.ResourcesPath como "Resources".

Isso faria com que os serviços de localização procurassem no diretório Recursos para arquivos de recursos.

Usar IStringLocalizer<T> e IStringLocalizerFactory

Depois de registrar (e, opcionalmente, configurar) os serviços de localização, você pode usar os seguintes tipos com a DI:

Para criar um serviço de mensagem que possa retornar as cadeias de caracteres localizadas, considere o seguinte MessageService:

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Localization;

namespace Localization.Example;

public sealed class MessageService(IStringLocalizer<MessageService> localizer)
{
    [return: NotNullIfNotNull(nameof(localizer))]
    public string? GetGreetingMessage()
    {
        LocalizedString localizedString = localizer["GreetingMessage"];

        return localizedString;
    }
}

No código anterior do C#:

  • Um campo IStringLocalizer<MessageService> localizer é declarado.
  • O construtor primário define um parâmetro IStringLocalizer<MessageService> e o captura como um argumento localizer.
  • O método GetGreetingMessage invoca o IStringLocalizer.Item[String], passando "GreetingMessage" como argumento.

O IStringLocalizer também é compatível com os recursos de cadeia de caracteres parametrizados, considere o seguinte ParameterizedMessageService:

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Localization;

namespace Localization.Example;

public class ParameterizedMessageService(IStringLocalizerFactory factory)
{
    private readonly IStringLocalizer _localizer =
        factory.Create(typeof(ParameterizedMessageService));

    [return: NotNullIfNotNull(nameof(_localizer))]
    public string? GetFormattedMessage(DateTime dateTime, double dinnerPrice)
    {
        LocalizedString localizedString = _localizer["DinnerPriceFormat", dateTime, dinnerPrice];

        return localizedString;
    }
}

No código anterior do C#:

  • Um campo IStringLocalizer _localizer é declarado.
  • O construtor primário usa um parâmetro IStringLocalizerFactory, que é usado para criar um IStringLocalizer do tipo ParameterizedMessageService e atribui ao campo _localizer.
  • O método GetFormattedMessage invoca IStringLocalizer.Item[String, Object[]], passando "DinnerPriceFormat", um objeto dateTime e o dinnerPrice como argumentos.

Importante

O IStringLocalizerFactory não é necessário. Em vez disso, é preferível que os serviços de consumo exijam o IStringLocalizer<T>.

Ambos os indexadores IStringLocalizer.Item[] retornam um LocalizedString, que tem conversões implícitas em string?.

Colocar tudo isso junto

Para exemplificar um aplicativo usando ambos os serviços de mensagem, juntamente com a localização e os arquivos de recurso, considere o seguinte arquivo Program.cs:

using System.Globalization;
using Localization.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using static System.Console;
using static System.Text.Encoding;

[assembly: RootNamespace("Localization.Example")]

OutputEncoding = Unicode;

if (args is [var cultureName])
{
    CultureInfo.CurrentCulture =
        CultureInfo.CurrentUICulture =
            CultureInfo.GetCultureInfo(cultureName);
}

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddLocalization();
builder.Services.AddTransient<MessageService>();
builder.Services.AddTransient<ParameterizedMessageService>();
builder.Logging.SetMinimumLevel(LogLevel.Warning);

using IHost host = builder.Build();

IServiceProvider services = host.Services;

ILogger logger =
    services.GetRequiredService<ILoggerFactory>()
        .CreateLogger("Localization.Example");

MessageService messageService =
    services.GetRequiredService<MessageService>();
logger.LogWarning(
    "{Msg}",
    messageService.GetGreetingMessage());

ParameterizedMessageService parameterizedMessageService =
    services.GetRequiredService<ParameterizedMessageService>();
logger.LogWarning(
    "{Msg}",
    parameterizedMessageService.GetFormattedMessage(
        DateTime.Today.AddDays(-3), 37.63));

await host.RunAsync();

No código anterior do C#:

  • Os RootNamespaceAttribute define "Localization.Example" como o namespace raiz.
  • O Console.OutputEncoding é atribuído a Encoding.Unicode.
  • Quando um único argumento é passado para args, o CultureInfo.CurrentCulture e o CultureInfo.CurrentUICulture são atribuídos ao resultado de CultureInfo.GetCultureInfo(String), considerando o arg[0].
  • O Host é criado com padrões.
  • Os serviços de localização, MessageService, e o ParameterizedMessageService estão registrados no IServiceCollection para DI.
  • Para remover o ruído, o log é configurado para ignorar qualquer nível de log inferior a um aviso.
  • O MessageService é resolvido na instância IServiceProvider e a mensagem resultante é registrada.
  • O ParameterizedMessageService é resolvido na instância IServiceProvider e a mensagem formatada resultante é registrada.

Cada uma das classes *MessageService define um conjunto de arquivos .resx, cada um com uma única entrada. Este é o conteúdo de exemplo para os MessageService arquivos de recurso, começando com MessageService.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="GreetingMessage" xml:space="preserve">
    <value>Hi friends, the ".NET" developer community is excited to see you here!</value>
  </data>
</root>

MessageService.sr-Cyrl-RS.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="GreetingMessage" xml:space="preserve">
    <value>Здраво пријатељи, ".NЕТ" девелопер заједница је узбуђена што вас види овде!</value>
  </data>
</root>

MessageService.sr-Latn.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="GreetingMessage" xml:space="preserve">
    <value>Zdravo prijatelji, ".NET" developer zajednica je uzbuđena što vas vidi ovde!</value>
  </data>
</root>

Este é o conteúdo de exemplo para os ParameterizedMessageService arquivos de recurso, começando com ParameterizedMessageService.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="DinnerPriceFormat" xml:space="preserve">
    <value>On {0:D} my dinner cost {1:C}.</value>
  </data>
</root>

ParameterizedMessageService.sr-Cyrl-RS.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="DinnerPriceFormat" xml:space="preserve">
    <value>У {0:D} моја вечера је коштала {1:C}.</value>
  </data>
</root>

ParameterizedMessageService.sr-Latn.resx:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="DinnerPriceFormat" xml:space="preserve">
    <value>U {0:D} moja večera je koštala {1:C}.</value>
  </data>
</root>

Dica

Todos os comentários do XML do arquivo de recurso, esquemas e elementos <resheader> são omitidos intencionalmente para abreviar.

Execuções de exemplo

As execuções de exemplo a seguir mostram as várias saídas localizadas, considerando as localidades de destino.

Considere "sr-Latn":

dotnet run --project .\example\example.csproj sr-Latn

warn: Localization.Example[0]
      Zdravo prijatelji, ".NET" developer zajednica je uzbuđena što vas vidi ovde!
warn: Localization.Example[0]
      U utorak, 03. avgust 2021. moja večera je koštala 37,63 ¤.

Ao omitir um argumento para a CLI do .NET para executar o projeto, a cultura do sistema padrão é usada. Nesse caso, "en-US":

dotnet run --project .\example\example.csproj

warn: Localization.Example[0]
      Hi friends, the ".NET" developer community is excited to see you here!
warn: Localization.Example[0]
      On Tuesday, August 3, 2021 my dinner cost $37.63.

Ao passar "sr-Cryl-RS", os arquivos de recurso correspondentes corretos são encontrados e a localização aplicada:

dotnet run --project .\example\example.csproj sr-Cryl-RS

warn: Localization.Example[0]
      Здраво пријатељи, ".NЕТ" девелопер заједница је узбуђена што вас види овде!
warn: Localization.Example[0]
      У уторак, 03. август 2021. моја вечера је коштала 38 RSD.

O aplicativo de exemplo não fornece os arquivos de recurso para "fr-CA", mas quando chamado com essa cultura, os arquivos de recurso não localizados são usados.

Aviso

Como a cultura é encontrada, mas os arquivos de recurso corretos não são, quando a formatação é aplicada, você acaba com a localização parcial:

dotnet run --project .\example\example.csproj fr-CA

warn: Localization.Example[0]
     Hi friends, the ".NET" developer community is excited to see you here!
warn: Localization.Example[0]
     On mardi 3 août 2021 my dinner cost 37,63 $.

Confira também