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:
- Varredura do código-fonte .NET para identificar strings chinesas.
- Geração de uma lista de termos candidatos (chave e valor padrão).
- 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.