Estendendo Meu Blog com Traduções pelo Amazon Nova

Este arquivo foi traduzido automaticamente por IA, erros podem ocorrer
Você está lendo este post em inglês? Ou talvez em espanhol? Ou por que não em italiano?
No meu post anterior sobre automação de tarefas para o meu blog, adicionei revisão automatizada usando o Amazon Nova. Embora escrever em inglês me permita conectar-me com um grande público técnico, há uma comunidade global ainda maior que poderia se beneficiar do meu conteúdo se estivesse disponível em seus idiomas nativos.
Eu já estendi meu blog com geração de voz usando o Amazon Polly. Fiz isso para que pessoas com dificuldades de leitura, como aquelas com dislexia, pudessem aprender com meu conteúdo sem dificuldades de leitura, mas ouvindo-o.
Desta vez, estou quebrando barreiras linguísticas e tornando meu conteúdo técnico acessível a desenvolvedores e entusiastas de cloud em todo o mundo através de tradução automatizada usando o Amazon Nova.
Visão
A comunidade técnica é incrivelmente diversa, e embora o inglês seja principalmente usado no mundo da tecnologia, longe de todos são falantes nativos de inglês. Eu não sou, e longe de todos estão 100% confortáveis consumindo conteúdo em inglês. Eles preferem lê-lo em seu idioma nativo. Conceitos técnicos complexos podem ser desafiadores o suficiente sem a barreira adicional da compreensão do idioma.
No meu post Solução de estatísticas serverless com Lambda@Edge, explico como construí um sistema de análise serverless para o meu blog, onde pude ver de onde meus leitores são, e mais.
Olhando para minhas análises, posso ver muitos leitores da Alemanha, Áustria, França, Itália, Espanha (e outros países de língua espanhola), e Brasil e Portugal. Portanto, queria criar uma solução onde as pessoas pudessem escolher se preferem ler em inglês ou em um idioma diferente.
Para esta primeira implementação de um serviço de tradução, escolhi suportar cinco idiomas que representam uma grande parte dos meus leitores: alemão, espanhol, francês, italiano e português. Posso adicionar idiomas adicionais como japonês em uma segunda fase.
O importante é que a solução:
Mantenha a precisão técnica: Termos técnicos e nomes de serviços da AWS precisam ser traduzidos corretamente.
Preserve a estrutura markdown: Toda formatação, blocos de código, links e imagens devem permanecer intactos.
Mantenha meu estilo de escrita: A tradução deve parecer natural enquanto mantém a profundidade técnica.
Escale eficientemente: Suporte múltiplos idiomas sem multiplicar minha carga de trabalho.
Integre-se perfeitamente: Encaixe-se no meu pipeline serverless e orientado a eventos existente.
Amazon Nova para Tradução
Tendo usado com sucesso o Amazon Nova Pro para revisão, eu tinha certeza de que ele seria capaz de lidar com minhas tarefas de tradução também. As capacidades multimodais do Nova Pro e a compreensão do contexto o tornam bem adequado para traduzir conteúdo técnico. Você pode ler mais sobre Nova, diferentes modelos e BedRock no meu post revisão automatizada usando o Amazon Nova.
Arquitetura da Solução
Com base na minha arquitetura orientada a eventos existente, estendi o pipeline para incluir a tradução. Quando criei o pipeline, construí-o de forma muito modular, para que pudesse ser facilmente estendido. Esta abordagem realmente compensa agora, pois posso conectar extensões perfeitamente no mesmo fluxo com muito pouco esforço.
As traduções são executadas em paralelo com a geração de voz e quiz no caso de execução de um pull request. No entanto, decidi separá-lo do fluxo saga padrão que uso para voz e quiz. A razão para isso é que gostaria de poder executar as traduções não apenas em pull requests, mas também invocar traduções de posts mais antigos.
Então, no caso do contexto de pull request, o fluxo começa quando a revisão é concluída, por um evento do GitHub Actions. Mas também posso invocar e iniciar o fluxo enviando um evento com um nome de branch e o blog para traduzir, para o serviço de informações que então inicia todo o fluxo. Uma adição nesse caso é que, em uma última etapa, também abrirei um novo pull request com as traduções para o post antigo.
As traduções são armazenadas em diretórios específicos de idioma (por exemplo, /de/
, /es/
, /fr/
), o que me permite organizar tudo durante o processo de build. Eu uso 11ty para gerar HTML estático a partir dos meus posts escritos em markdown.
Mergulho Técnico Profundo
A tradução é implementada como seu próprio serviço e é baseada em Step Functions como o serviço principal e orquestrador. A tradução começa ou comigo criando um novo branch e iniciando o fluxo para um post mais antigo, ou é automaticamente iniciada quando um novo pull request é aberto, para um novo post. Criando um fluxo de tradução assim.
Step Function de Tradução
O núcleo do serviço de tradução é esta Step Function, que orquestra todo o fluxo. Se você me segue há algum tempo, sabe que normalmente tento usar as integrações do SDK de Step Functions o máximo possível, mas para o fluxo de tradução, preciso confiar muito no meu segundo serviço favorito, o Lambda.
O workflow consiste em vários estados-chave:
- CollectInfo: Normaliza os dados de entrada e prepara para traduções, lê idiomas do evento ou usa o padrão.
- CheckoutMarkdownFile: Faz o download do arquivo markdown de origem do GitHub.
- Iterate Languages: Executa traduções para os idiomas de destino, limitado a 1 tradução paralela no momento.
- CreateCommit: Faz o commit de todos os arquivos traduzidos de volta ao repositório, abre um novo pull request se necessário.
Olhando para a definição da Step Function, estou usando o suporte para a linguagem de consulta JSONata, que é uma das melhores adições às Step Functions junto com as variáveis. O estado CollectInfo
armazenará informações em variáveis que estarão disponíveis para todos os estados futuros. Iterando sobre idiomas, estou usando um estado Map
com um limite definido para 1.
Comment: Traduzir o conteúdo do post usando Bedrock.
QueryLanguage: JSONata
StartAt: CollectInfo
States:
CollectInfo:
Type: Pass
Assign:
Languages: "{% $exists($states.input.detail.Data.Translate.Languages) ? $states.input.detail.Data.Translate.Languages : ['de','es','fr','it','pt'] %}"
GitInfo:
CommitSha: "{% $exists($states.input.detail.Data.GitInfo.CommitSha) ? $states.input.detail.Data.GitInfo.CommitSha : $states.input.detail.Data.PullRequestInfo.PullRequestCommitSha %}"
Branch: "{% $exists($states.input.detail.Data.GitInfo.Branch) ? $states.input.detail.Data.GitInfo.Branch : $states.input.detail.Data.PullRequestInfo.PullRequestBranch %}"
MarkdownInfo:
FilePath: "{% $states.input.detail.Data.MarkdownFile.path %}"
FileSlug: "{% $states.input.detail.Data.MarkdownFile.fileSlug %}"
Next: CheckoutMarkdownFile
CheckoutMarkdownFile:
Type: Task
Resource: arn:aws:states:::lambda:invoke
Arguments:
FunctionName: ${CheckoutMarkdownFileFunctionArn}
Payload:
GitInfo: "{% $GitInfo %}"
MarkdownInfo: "{% $MarkdownInfo %}"
Next: Iterate Languages
Iterate Languages:
Type: Map
ItemProcessor:
ProcessorConfig:
Mode: INLINE
StartAt: TranslateMarkdownFile
States:
TranslateMarkdownFile:
Type: Task
Resource: arn:aws:states:::lambda:invoke
Arguments:
FunctionName: ${TranslateBedrockFunctionArn}
Payload:
Key: "{%$MarkdownInfo.FileSlug & '/' & $MarkdownInfo.FilePath %}"
S3Bucket: ${ETLBucket}
Language: "{% $states.input %}"
End: true
Next: CreateCommit
MaxConcurrency: 1
Items: "{% $Languages %}"
CreateCommit:
Type: Task
Resource: arn:aws:states:::lambda:invoke
Arguments:
FunctionName: ${CreateCommitFunctionArn}
Payload:
GitInfo: "{% $GitInfo %}"
MarkdownInfo: "{% $MarkdownInfo %}"
End: true
Funções Lambda
Olhando para as funções Lambda, há três funções principais em jogo na solução. A função que usará o Octokit para fazer o checkout do código, e a função que criará o novo commit. Ambas foram explicadas em posts anteriores sobre este tópico e são omitidas desta vez.
Função de Tradução
Este é o coração e a alma de toda a solução e interagirá com o Bedrock para traduzir o post. O post traduzido será armazenado em um diretório específico do idioma.
import json
import os
import boto3
from botocore.exceptions import ClientError
def handler(event, context):
try:
bucket = event.get("S3Bucket")
key = event.get("Key")
lang = event.get("Language", "error")
if lang not in ["de", "es", "fr", "it", "pt"]:
raise ValueError(
f"Idioma não suportado '{lang}'. Idiomas suportados são: de, es, fr, it, pt."
)
s3_client = boto3.client("s3")
bedrock_client = boto3.client("bedrock-runtime", region_name="eu-west-1")
response = s3_client.get_object(Bucket=bucket, Key=key)
file_content = response["Body"].read().decode("utf-8")
prompt = f"""Você é um editor técnico profissional e tradutor, especialista em AWS e Cloud computing. Por favor, traduza cuidadosamente o seguinte post de blog em markdown para {lang}.
Instruções:
- Mantenha a formatação markdown original
- Mantenha o tom e o estilo consistentes
- Não mude o significado ou a estrutura do conteúdo
- Retorne apenas o texto markdown traduzido sem nenhum comentário adicional
- Não traduza nenhum bloco de código ou código inline
- Garanta gramática, ortografia e pontuação adequadas
- Se o conteúdo já estiver em {lang}, apenas ignore e retorne o conteúdo como está
- Não envolva a versão traduzida com ```markdown
Aqui está o conteúdo markdown para traduzir:
{file_content}"""
# Prepare o pedido para o modelo Nova Pro
request_body = {
"messages": [{"role": "user", "content": [{"text": prompt}]}],
"inferenceConfig": {
"temperature": 0.1, # Temperatura baixa para traduções consistentes
"topP": 0.9,
"maxTokens": 10240, # Ajuste com base no comprimento de saída esperado
},
}
bedrock_response = bedrock_client.invoke_model(
modelId="eu.amazon.nova-pro-v1:0",
body=json.dumps(request_body),
contentType="application/json",
)
response_body = json.loads(bedrock_response["body"].read())
translated_content = response_body["output"]["message"]["content"][0]["text"]
new_key = f"{lang}/{key}"
s3_client.put_object(
Bucket=bucket,
Key=new_key,
Body=translated_content.encode("utf-8"),
ContentType="text/markdown",
)
return {
"statusCode": 200,
"body": "Tradução concluída",
}
except Exception as e:
print(f"Erro inesperado: {str(e)}")
return {
"statusCode": 500,
"body": json.dumps({"error": "Erro Interno do Servidor", "message": str(e)}),
}
Qualidade da Tradução e Engenharia de Prompt
Durante a implementação da solução de revisão, aprendi o quão importante era a parte de engenharia de prompt para o resultado final. A IA ainda faz apenas o que você lhe diz para fazer, e se seu prompt não for específico o suficiente, a qualidade do resultado pode variar. Usando os aprendizados da revisão, meu prompt incluiu alguns requisitos específicos.
No final, o seguinte prompt me deu ótimos resultados:
Você é um editor técnico profissional e tradutor, especialista em AWS e Cloud computing.
Por favor, traduza cuidadosamente o seguinte post de blog em markdown para {lang}.
Instruções:
- Mantenha a formatação markdown original
- Mantenha o tom e o estilo consistentes
- Não mude o significado ou a estrutura do conteúdo
- Retorne apenas o texto markdown traduzido sem nenhum comentário adicional
- Não traduza nenhum bloco de código ou código inline
- Garanta gramática, ortografia e pontuação adequadas
- Se o conteúdo já estiver em {lang}, apenas ignore e retorne o conteúdo como está
- Não envolva a versão traduzida com ```markdown
Aqui está o conteúdo markdown para traduzir:
Mudanças no 11ty
Estou usando o 11ty como o gerador de HTML estático, todos os blogs são escritos em markdown e depois convertidos para HTML. Para poder suportar diferentes idiomas, precisei fazer algumas mudanças no meu build e layout do 11ty. Primeiro de tudo, adicionei um novo campo ao front matter que indica em que idioma o post está. Então precisei mudar como minha coleção de posts estava buscando posts de blog.
Adicionei um filtro para buscar apenas os posts em inglês para não obter todos os posts na coleção.
export default (coll) => {
const posts = coll
.getFilteredByGlob("src/posts/*.md")
.filter((p) => (p.data.lang || "en") === "en");
return posts.reverse();
};
Em seguida, precisei localizar todas as traduções para um post. Usei o slug do arquivo como chave para criar esta coleção.
config.addCollection("postsBySlug", (collection) => {
const posts = collection.getFilteredByGlob("src/posts/**/*.md");
const postsBySlug = {};
posts.forEach((post) => {
const pathParts = post.inputPath.split("/");
const filename = pathParts[pathParts.length - 1];
const baseSlug = filename.replace(".md", "");
if (!postsBySlug[baseSlug]) {
postsBySlug[baseSlug] = [];
}
postsBySlug[baseSlug].push(post);
});
return postsBySlug;
});
Com isso em vigor, pude criar e incluir um indicador de idioma que adicionaria bandeiras indicando traduções à grade de posts. Onde cada bandeira é clicável e levaria você diretamente ao post traduzido.
{% set item = post or talk %}
{% set pathParts = item.inputPath.split('/') %}
{% set filename = pathParts[pathParts.length - 1] %}
{% set baseSlug = filename.replace('.md', '') %}
{% set currentLang = item.data.lang or 'en' %}
{# Apenas mostre o indicador de idioma para posts (não palestras ou links) #}
{% if item.inputPath.includes('/posts/') %}
{% set availablePosts = collections.postsBySlug[baseSlug] %}
{% if availablePosts.length > 1 %}
<div class="mx-auto flex items-center flex-wrap">
<div class="justify-start items-center flex-grow w-full md:w-auto md:flex">
<p class="text-gray-700 mb-1 font-medium" title="Idiomas disponíveis">
<i class="fas fa-globe text-teal-600 fill-current"></i>
{% for translatedPost in availablePosts %}
{% set postLang = translatedPost.data.lang or 'en' %}
<a href="{{ translatedPost.url }}" class="text-sm ml-1 hover:scale-110 transition-transform duration-200 inline-block" title="{{ postLang | getLanguageName }}">{{ postLang | getLanguageFlag }}</a>
{% endfor %}
</p>
</div>
</div>
{% endif %}
{% endif %}
Resultando em:
Finalmente, adicionei um menu suspenso ao layout do post real junto com um aviso de que o post foi traduzido por uma IA.
Com essas mudanças, você pode selecionar em que idioma deseja ler cada post.
Conclusão
Adicionar tradução automatizada ao meu blog pareceu o próximo passo para tornar meu conteúdo disponível para todos. Ao aproveitar o Amazon Nova Pro e minha arquitetura serverless existente, criei uma solução de tradução escalável sem adicionar complexidade operacional. Seja você esteja construindo um blog técnico, documentação ou qualquer plataforma de conteúdo, a tradução automatizada poderia expandir seu alcance.
Por que não traduzi o site inteiro? Esta foi uma rota que explorei primeiro, mas senti que o principal benefício seria ter os posts traduzidos. Também me dá a liberdade de decidir se quero traduzir todos os posts mais antigos ou não, e posso adicionar traduções posteriormente. Um dia posso traduzir tudo, mas agora o foco estava nos posts individualmente.
Palavras Finais
Não se esqueça de me seguir no LinkedIn e para mais conteúdo, leia o resto dos meus Blogs.
Como Werner diz! Agora Vá Construir!
Este post foi originalmente escrito em inglês e automaticamente traduzido para múltiplos idiomas usando o próprio sistema descrito aqui. O processo de tradução levou menos de 5 minutos e não exigiu nenhuma intervenção manual.