Internacionalização de Aplicações .NET: Extração e Substituição Automática de Termos com Roslyn

Em sistemas de aplicação modernos, a internacionalização é um requisito crucial para atingir um público global. No contexto de aplicações baseadas em .NET e Vue.js, uma abordagem eficiente envolve a automação da identificação de textos em chinesês no código-fonte e a sua substituição por chamadas a serviços de tradução. Este artigo explora a implementação de uma ferramenta baseada no compilador Roslyn para realizar esse processo de extração e atualização de código de forma programática.

Contexto e Requisitos do Projeto

Em muitos sistemas legados baseados em .NET, os textos de interface, mensagens de erro e notificações são frequentemente codificados diretamente como strings literais em chinês. Para tornar a aplicação multilíngue, é necessário localizar todas essas ocorrências, convertê-las em termos gerenciados e alterar o código para buscar as traduções dinamicamente.

O objetivo final é um pipeline automatizado com as seguintes etapas:

  1. Varredura do código-fonte .NET para identificar strings chinesas.
  2. Geração de uma lista de termos candidatos (chave e valor padrão).
  3. Substituição das strings litreais por chamadas ao serviço de termos de internacionalização.

Considere o seguinte trecho de código como ponto de partida:

private void ValidarArquivo(string nomeArquivo, string hashMD5)
{
    string hashCalculado = ServicoMD5.Calcular(nomeArquivo);
    if (!string.Equals(hashCalculado, hashMD5, StringComparison.OrdinalIgnoreCase))
    {
        throw new ExcecaoCustomizada(CodigosErro.FalhaValidacaoMD5,
            "A verificação MD5 do arquivo do pacote falhou: " + nomeArquivo);
    }
}

O texto em negrito precisa ser substituído por uma chamada que obtenha a tradução adequada, por exemplo:

var mensagemErro = ServicoTermos.ObterTextoFormatado("ERRO_FALHA_MD5_PACOTE", "A verificação MD5 do arquivo do pacote falhou:");
throw new ExcecaoCustomizada(CodigosErro.FalhaValidacaoMD5, mensagemErro + nomeArquivo);

Implementação da Varredura de Código com Roslyn

O Roslyn, a plataforma de compiladores do .NET, fornece APIs poderosas para analisar e transformar código. A primeira etapa é definir uma classe para armazenar os resultados da varredura.

public class ResultadoVarreduraTermo
{
    public Guid Identificador { get; set; }
    public string TextoOriginal { get; set; }
    public string TextoChines { get; set; }
    public string CaminhoSolucao { get; set; }
    public string NomeProjeto { get; set; }
    public string CaminhoArquivoClasse { get; set; }
    public string NomeMetodo { get; set; }
    public string TrechoCodigo { get; set; }
    public TermoI18N TermoTraduzido { get; set; }
}

O método principal de varredura itera sobre todos os projetos em uma solução (.sln). Para cada documento .cs, ele analisa a árvore sintática.

public async Task<List<ResultadoVarreduraTermo>> EscanearSolucao(string caminhoSolucao)
{
    var resultados = new List<ResultadoVarreduraTermo>();
    MSBuildLocator.RegisterDefaults();
    using var workspace = MSBuildWorkspace.Create();
    var solucao = await workspace.OpenSolutionAsync(caminhoSolucao);

    foreach (var projeto in solucao.Projetos)
    {
        foreach (var documento in projeto.Documents.Where(d => d.Name.EndsWith(".cs")))
        {
            var arvoreSintatica = await documento.GetSyntaxTreeAsync();
            var raiz = arvoreSintatica.GetCompilationUnitRoot();

            // Localiza todas as expressões de string literais
            var expressoesString = raiz.DescendantNodes()
                .OfType<LiteralExpressionSyntax>()
                .Where(ExisteCaractereChines);

            foreach (var expr in expressoesString)
            {
                var texto = expr.Token.ValueText;
                if (string.IsNullOrWhiteSpace(texto)) continue;

                resultados.Add(new ResultadoVarreduraTermo
                {
                    Identificador = Guid.NewGuid(),
                    TextoOriginal = texto,
                    TextoChines = expr.ToString(),
                    CaminhoSolucao = caminhoSolucao,
                    NomeProjeto = projeto.Name,
                    CaminhoArquivoClasse = documento.FilePath,
                    TrechoCodigo = expr.Parent?.ToString(),
                    TermoTraduzido = null // Será preenchido em etapa posterior
                });
            }
        }
    }
    return resultados;
}

private bool ExisteCaractereChines(LiteralExpressionSyntax literal)
{
    // Verifica se o token é uma string literal e contém caracteres no intervalo Unicode do chinês.
    return literal.IsKind(SyntaxKind.StringLiteralExpression) &&
           Regex.IsMatch(literal.Token.ValueText, @"[\u4e00-\u9fa5]");
}

Processo de Substituição de Código

Com a lista de termos identificados e (potencialmente) traduzidos, a etapa seguinte é reescrever o código. Isso é feito usando a reescrita de sintaxe do Roslyn.

public class ReescritorCodigoFonte
{
    public async Task SubstituirTermosNaSolucao(
        string caminhoSolucao,
        List<ResultadoVarreduraTermo> termosEscaneados)
    {
        // ... (código de inicialização do workspace similar ao anterior)

        foreach (var documento in todosOsDocumentosCS)
        {
            var arvoreSintatica = await documento.GetSyntaxTreeAsync();
            var raiz = arvoreSintatica.GetCompilationUnitRoot() as CompilationUnitSyntax;
            var reescritor = new SubstituidorExpressoesString(termosEscaneados);
            var novaRaiz = reescritor.Visit(raiz);

            // Salva o arquivo modificado
            var textoFormatado = novaRaiz.NormalizeWhitespace().ToFullString();
            File.WriteAllText(documento.FilePath, textoFormatado);
        }
    }
}

// Visitor pattern para substituir expressões específicas
public class SubstituidorExpressoesString : CSharpSyntaxRewriter
{
    private readonly Dictionary<string, TermoI18N> _mapaTermos;

    public SubstituidorExpressoesString(List<ResultadoVarreduraTermo> termos)
    {
        _mapaTermos = termos
            .Where(t => t.Term != null)
            .ToDictionary(t => t.TextoOriginal, t => t.Term);
    }

    public override SyntaxNode VisitLiteralExpression(LiteralExpressionSyntax node)
    {
        if (node.IsKind(SyntaxKind.StringLiteralExpression) &&
            _mapaTermos.TryGetValue(node.Token.ValueText, out var termo))
        {
            // Constrói a expressão de chamada ao serviço: ServicoTermos.ObterTexto("CHAVE")
            var chamadaMetodo = SyntaxFactory.InvocationExpression(
                SyntaxFactory.MemberAccessExpression(
                    SyntaxKind.SimpleMemberAccessExpression,
                    SyntaxFactory.IdentifierName("ServicoTermos"),
                    SyntaxFactory.IdentifierName("ObterTexto")))
                .WithArgumentList(
                    SyntaxFactory.ArgumentList(
                        SyntaxFactory.SingletonSeparatedList(
                            SyntaxFactory.Argument(
                                SyntaxFactory.LiteralExpression(
                                    SyntaxKind.StringLiteralExpression,
                                    SyntaxFactory.Literal(termo.Codigo))))));

            return chamadaMetodo;
        }
        return base.VisitLiteralExpression(node);
    }
}

O reescritor percorre a árvore sintática, encontra as strings literais que correspondem aos termos extraídos e as substitui por invocações de método que consultam o serviço de termos, garantindo que a tradução seja resolvida em tempo de execução.

Tags: Roslyn .NET Internacionalização análise de código Transformação de Código

Publicado em 7-24 16:14