Alejandro Rioja.
AI Agents SEO

Cómo Traducir un Post a 13 Idiomas con un Solo Agente

Alejandro Rioja
Alejandro Rioja
7 min de lectura
TL;DR

Un solo agente en TypeScript llama a la API de Claude en paralelo para traducir un post en inglés a 12 idiomas en menos de 90 segundos. Preservar la voz requiere un system prompt en dos partes: restricciones de estilo primero, luego notas por idioma. El costo es aproximadamente $0.004–$0.02 por post con Haiku. Mi sitio tuvo un aumento del 34% en tráfico internacional en 60 días.

Newsletter gratuita

Cada miércoles. 28.400+ operadores. Sin relleno.

Tabla de contenidos

Actualizado mayo 2026.

TL;DR: Un solo agente en TypeScript llama a la API de Claude en paralelo para traducir un post en inglés a 12 idiomas en menos de 90 segundos. Preservar la voz requiere un system prompt en dos partes: restricciones de estilo primero, luego notas por idioma. El costo es aproximadamente $0.004–$0.02 por post con Haiku. Mi sitio tuvo un aumento del 34% en tráfico internacional en 60 días.

[Perspectiva del operador] Ejecuto este agente cada vez que publico un post nuevo. Ha procesado 341 posts en 12 idiomas sin que yo haya tocado una sola traducción manualmente. Así es exactamente como funciona.

Por qué construí un agente de traducción en lugar de contratar traductores

Me salto el argumento de venta sobre SEO multilingüe — ya sabes que importa. El problema que tenía era de flujo de trabajo. Contratar traductores por post es caro ($40–$120/post × 12 idiomas = $480–$1,440 por artículo), lento (3–7 días de entrega) e imposible de procesar en lote cuando tienes 341 posts existentes por traducir.

La otra opción que sugiere la gente es Google Translate o DeepL. Ambas son precisas pero destruyen la voz. Mi estilo de escritura es directo, en primera persona y ligeramente contrariano. La traducción automática tiende a hacer todo sonar formal y pasivo. Eso es un problema cuando la consistencia de voz es parte de tu marca.

Así que construí un agente en TypeScript respaldado por Claude. Se ejecuta en CI en cada merge a main, distribuye las traducciones en paralelo, escribe los archivos de vuelta al disco y salta cualquier idioma que ya tenga un archivo. Todo tarda menos de 90 segundos para un post nuevo.

La estructura del proyecto

El agente vive en scripts/agent/translate-worker.ts. Lo llama un orquestador de nivel superior que lee el post en inglés, extrae el frontmatter y despacha un trabajo de traducción por idioma.

code
scripts/
  agent/
    translate-worker.ts   # lógica de traducción por idioma
    translate-all.ts      # orquestador: lee EN, distribuye a 12 idiomas
    lib/
      frontmatter.ts      # parsear/serializar frontmatter con gray-matter
      voice-prompt.ts     # constructor de system prompt compartido

El orquestador (translate-all.ts) usa Promise.allSettled para que un solo idioma fallido no bloquee el resto.

La ingeniería del system prompt

Aquí es donde la mayoría de la gente se equivoca. Escriben algo como “traduce esto al francés, mantén la voz del autor.” Eso produce resultados mediocres.

Mi system prompt tiene dos secciones obligatorias:

Sección 1 — Restricciones de estilo (universal, añadidas a cada llamada):

typescript
// scripts/agent/lib/voice-prompt.ts
export function buildSystemPrompt(targetLocale: string): string {
  const styleConstraints = `
You are a professional translator working on blog posts written by Alejandro Rioja.

STYLE RULES — apply to every locale:
- Short paragraphs (1–3 sentences max). Do not merge them.
- First-person, direct voice. Never passive if active is natural.
- No filler phrases: no "In today's world", no "It is worth noting that".
- Preserve all markdown: headings, bold, italics, code blocks, links.
- Translate heading text but keep the ## / ### prefix exactly.
- Code blocks: translate comments only. Keep all variable names, strings, and syntax in English.
- Preserve frontmatter keys exactly. Only translate the VALUES for: title, ogTitle, description, tldr, imageAlt.
- Keep these frontmatter values UNCHANGED: pubDate, updatedDate, translation_key, tags, image, author, draft, lang (set lang to: ${targetLocale}).
`.trim();

Sección 2 — Notas específicas por idioma (añadidas por llamada):

typescript
  const localeNotes: Record<string, string> = {
    ar: "Arabic: use Modern Standard Arabic (MSA). RTL layout is handled by the CMS — do not add any RTL markup. Avoid overly formal Classical Arabic registers.",
    de: "German: use informal 'du' not formal 'Sie'. Compound nouns are fine; don't over-hyphenate. Keep tech terms in English when that's the industry standard (e.g. 'Content Marketing', 'SEO').",
    es: "Spanish: use neutral Latin American Spanish, not Castilian. Tuteo ('tú') over 'usted'. Keep anglicisms that are standard in tech (SEO, agente, prompt).",
    fr: "French: use informal 'tu'. Avoid over-formalizing. Tech anglicisms are acceptable when widely used (SEO, agent, prompt).",
    hi: "Hindi: use Devanagari script. Mix Hindi and English naturally for tech terms — this is standard in Indian tech writing. Don't force Hindi equivalents for words like 'agent', 'prompt', 'SEO'.",
    it: "Italian: use 'tu' form. Keep English tech terms where they're standard in Italian digital marketing.",
    ja: "Japanese: use です/ます (polite) style, not casual or keigo. Keep technical English terms in katakana where standard (e.g. エージェント, プロンプト, SEO).",
    ko: "Korean: use 합쇼체 (formal polite). Tech terms in English or standard Korean loanwords. Keep SEO, agent, prompt as-is or standard loanwords.",
    nl: "Dutch: use 'je/jij' (informal). Keep English tech terms standard in Dutch digital marketing.",
    pt: "Portuguese: use Brazilian Portuguese (pt-BR). Informal 'você'. Keep tech anglicisms standard in Brazilian digital marketing.",
    ru: "Russian: use modern, accessible Russian. Avoid overly bureaucratic phrasing. Tech terms can stay in English where that's the norm in Russian tech writing.",
    zh: "Chinese: use Simplified Chinese (zh-CN). Modern, accessible tone. Tech terms can use standard Chinese equivalents or keep English where that's industry norm.",
  };

  return `${styleConstraints}\n\nLOCALE-SPECIFIC NOTES for ${targetLocale}:\n${localeNotes[targetLocale]}`;
}

El translate worker

Aquí está el worker completo. Lee el archivo en inglés, llama a Claude y escribe el resultado en disco.

typescript
// scripts/agent/translate-worker.ts
import Anthropic from "@anthropic-ai/sdk";
import * as fs from "fs";
import * as path from "path";
import { buildSystemPrompt } from "./lib/voice-prompt";

const client = new Anthropic();

export interface TranslateJob {
  enFilePath: string;
  locale: string;
  outputDir: string;
  model?: "claude-haiku-4-5" | "claude-sonnet-4-5";
  dryRun?: boolean;
}

export async function translatePost(job: TranslateJob): Promise<string> {
  const { enFilePath, locale, outputDir, model = "claude-haiku-4-5", dryRun = false } = job;

  // Idempotencia: saltar si la traducción ya existe
  const filename = path.basename(enFilePath);
  const outPath = path.join(outputDir, locale, filename);
  if (fs.existsSync(outPath)) {
    console.log(`[${locale}] Ya existe — saltando: ${outPath}`);
    return outPath;
  }

  const enContent = fs.readFileSync(enFilePath, "utf-8");
  const systemPrompt = buildSystemPrompt(locale);

  const message = await client.messages.create({
    model,
    max_tokens: 8192,
    system: systemPrompt,
    messages: [
      {
        role: "user",
        content: `Translate the following blog post to ${locale}. Return ONLY the translated markdown file content — no explanation, no preamble, no code fences around the whole file.\n\n${enContent}`,
      },
    ],
  });

  const translated = (message.content[0] as { type: string; text: string }).text;

  if (!dryRun) {
    fs.mkdirSync(path.join(outputDir, locale), { recursive: true });
    fs.writeFileSync(outPath, translated, "utf-8");
    console.log(`[${locale}] Escrito: ${outPath}`);
  }

  return outPath;
}

El orquestador

typescript
// scripts/agent/translate-all.ts
import * as path from "path";
import * as fs from "fs";
import { translatePost } from "./translate-worker";

const LOCALES = ["ar", "de", "es", "fr", "hi", "it", "ja", "ko", "nl", "pt", "ru", "zh"];
const POSTS_DIR = path.resolve("src/content/posts");
const MODEL = (process.env.TRANSLATE_MODEL as "claude-haiku-4-5" | "claude-sonnet-4-5") ?? "claude-haiku-4-5";

async function main() {
  // Acepta un archivo específico o traduce todos los posts en inglés
  const targetFile = process.argv[2];
  const enFiles = targetFile
    ? [path.resolve(targetFile)]
    : fs.readdirSync(path.join(POSTS_DIR, "en")).map((f) => path.join(POSTS_DIR, "en", f));

  console.log(`Traduciendo ${enFiles.length} post(s) × ${LOCALES.length} idiomas. Modelo: ${MODEL}`);

  for (const enFile of enFiles) {
    const results = await Promise.allSettled(
      LOCALES.map((locale) =>
        translatePost({
          enFilePath: enFile,
          locale,
          outputDir: POSTS_DIR,
          model: MODEL,
        })
      )
    );

    results.forEach((r, i) => {
      if (r.status === "rejected") {
        console.error(`[${LOCALES[i]}] FALLIDO:`, r.reason);
      }
    });
  }

  console.log("Listo.");
}

main();

Ejecutar con:

sh
# Traducir un post nuevo
npx ts-node scripts/agent/translate-all.ts src/content/posts/en/mi-nuevo-post.md

# Traducir todo (idempotente — salta los existentes)
npx ts-node scripts/agent/translate-all.ts

Desglose de costos: Haiku vs Sonnet

Esto es lo que cuesta realmente por post, basado en mi uso:

ModeloTokens de entrada (promedio)Tokens de salida (promedio)Costo por idiomaCosto × 12 idiomas
claude-haiku-4-5~2,400~2,600~$0.0004~$0.005
claude-sonnet-4-5~2,400~2,600~$0.015~$0.18

Para 341 posts × 12 idiomas con Haiku: aproximadamente $1.70 en total. Eso es todo el backlog.

Sonnet produce una fraseología idiomática marginalmente mejor, pero para la mayoría de los posts la diferencia no vale 36 veces el precio. Uso Sonnet solo para posts donde el tono persuasivo matizado importa — como páginas de ventas o contenido principal de alto tráfico.

Puedes cambiar los modelos por ejecución con la variable de entorno TRANSLATE_MODEL:

sh
TRANSLATE_MODEL=claude-sonnet-4-5 npx ts-node scripts/agent/translate-all.ts src/content/posts/en/post-insignia.md

Resultados reales: qué pasó con mi tráfico

Publiqué la traducción completa del backlog (341 posts) en diciembre de 2025. En 60 días:

Los resultados de japonés y coreano me sorprendieron. Ambos idiomas tienen comunidades de IA de alta calidad y aparentemente buena demanda de contenido práctico de operadores.

La conclusión del operador

Un agente, una hora de configuración, $1.70 en costos de API. Eso es lo que se necesitó para hacer que 341 posts fueran descubribles en 12 idiomas adicionales. El aumento de SEO solo pagó el cómputo en la primera semana. Si tienes un sitio con mucho contenido y todavía no has construido esto, estás dejando tráfico internacional sobre la mesa. El código de arriba es la implementación completa — fórcalo, cambia las notas de tu voice-prompt y ejecútalo contra tu backlog esta noche.

Seguir leyendo

Recibe el manual de IA en tu buzón

Cada miércoles. 28.400+ operadores. Sin relleno.

↵ para ver todos los resultados esc esc para cerrar