使用 DeepL API 自动运行多语言博客

🌐

本帖由 DeepL 翻译。如有任何翻译错误,请告知我们!

博客背景

在运营技术博客时,内容的覆盖面是一个重要的考虑因素。特别是,仅用韩语撰写的内容覆盖面有限,限制了与全球开发人员的知识共享。事实上,根据开发人员的人口统计数据,韩国是排名最靠后的 15 个国家之一,这进一步强调了多语言支持的必要性。

Image.png

전문 개발자 수 기준 상위 15개국(参见:)。

对自动化的需求

现代人工智能翻译和传统翻译工具可以提供相当准确的翻译。但是,每次撰写新文章时都要手动翻译和上传,效率很低。特别是如果您不仅要提供英文版,还要提供中文版和日文版,而这些地方有许多开发人员在工作,那么手动操作就不太现实了。

为了解决这个问题,我们使用 DeepL API 实现了自动翻译脚本。

开发环境配置

安装必要的软件包,以便在 Next.js 环境中运行 Node.js 脚本。

pnpm install deepl-node dotenv tsx

起初,我尝试使用ts-node,但它与 Next.js 环境存在配置冲突。相反,我们使用tsx库建立了一个独立的执行环境。

项目结构

首先,我的项目结构大致如下。

.
├── src/
│   └── app/
│       └── posts/
│           └── [slug]/
│               └── page.tsx
├── posts/
│   └── post1.mdx
└── package.json

然后,我创建了这样一个脚本

import fs from "fs/promises";
import path from "path";

import * as deepl from "deepl-node";
import matter from "gray-matter";
import dotenv from "dotenv";

dotenv.config();

const DEEPL_API_KEY = process.env.DEEPL_API_KEY!;
const translator = new deepl.Translator(DEEPL_API_KEY);

const SOURCE_DIR = "src/posts";
const TARGET_DIR = "src/posts/en";

interface PostContent {
  content: string;
  data: {
    title: string;
    description: string;
    [key: string]: string;
  };
}

async function translatePost(content: {
  data: { [p: string]: string };
  content: string;
}): Promise<PostContent> {
  const translatedTitle = await translator.translateText(
    content.data.title,
    "ko",
    "en-US",
  );

  const translatedDescription = await translator.translateText(
    content.data.description,
    "ko",
    "en-US",
  );

  const translatedContent = await translator.translateText(
    content.content,
    "ko",
    "en-US",
  );

  return {
    content: translatedContent.text,
    data: {
      ...content.data,
      title: translatedTitle.text,
      description: translatedDescription?.text,
      originalLang: "ko",
    },
  };
}

async function processFile(filename: string) {
  try {
    const sourcePath = path.join(SOURCE_DIR, filename);
    const targetPath = path.join(TARGET_DIR, filename);

    // 파일 존재 여부 확인
    try {
      await fs.access(sourcePath);
    } catch (error) {
      throw new Error(`파일을 찾을 수 없습니다: ${filename}`);
    }

    // MDX 파일 읽기
    const fileContent = await fs.readFile(sourcePath, "utf-8");
    const { data, content } = matter(fileContent);

    // 번역 실행
    console.log(`${filename} 번역 중...`);
    const translated = await translatePost({ data, content });

    // 번역된 MDX 파일 생성
    const translatedFileContent = matter.stringify(
      translated.content,
      translated.data,
    );
    await fs.mkdir(TARGET_DIR, { recursive: true });
    await fs.writeFile(targetPath, translatedFileContent);

    console.log(`${filename} 번역 완료!`);
  } catch (error) {
    console.error(`Error:`, error);
    process.exit(1);
  }
}

// 명령줄 인자에서 파일명 가져오기
const filename = process.argv[2];
if (!filename) {
  process.exit(1);
}

// 파일 확장자 확인
if (!filename.endsWith(".mdx")) {
  console.error("Error: MDX 파일만 지원됩니다.");
  process.exit(1);
}

processFile(filename);

将脚本也添加到package.json中。

{
  "scripts": {
    "translate": "tsx scripts/translate-posts.ts"
  }
}

运行结果

现在,在终端中键入命令

Image.png

就会创建一个已翻译的 mdx 文件。

Image.png

问题

在最初的实施过程中,我们将 MDX 文件中的文本直接发送到 DeepL API,但发现了以下问题

  1. 破坏 Markdown 语法
  2. 不必要地翻译代码块
  3. 图像标签和链接结构失真
  • 原创

Image.png

  • 日语翻译

Image.png

如何解决

我一直在想该怎么办,于是想出了以下办法。HTML handling首先,我在 DeepL API 文档中看到了这部分内容,似乎以 HTML 发送文本可以很好地处理表单,而不会破坏表单。

因此,为了解决上述问题,我们引入了以下改进流程。

  1. 将 MDX 转换为 HTML
  2. 发送 DeepL API,并保留 HTML 标记选项
  3. 翻译 HTML → MDX 重新转换
const convertMDXToHtml = async (markdown: string) => {
  try {
    const html = await unified()
      .use(remarkParse)
      .use(remarkHtml)
      .process(markdown);

    return html.toString();
  } catch (err) {
    console.error("MD => HTML 변환 중 오류가 발생했습니다: ");
    return "error";
  }
};

const convertHtmlToMDX = async (html: string) => {
  try {
    const markdown = await unified()
      .use(rehypeParse)
      .use(rehypeRemark)
      .use(remarkStringify)
      .process(html);

    return markdown.toString();
  } catch (err) {
    console.error("HTML => MD 변환 중 오류가 발생했습니다: ");
    return "error";
  }
};

async function translatePost(
  content: { data: { [p: string]: string }; content: string },
  targetLang: TargetLanguageCode,
): Promise<PostContent> {
  // 중략...

  // md => html
  const html = await convertMDXToHtml(content.content);

  // html => translated html
  const translatedContent = await translator.translateText(
    html,
    "ko",
    targetLang,
    {
      tagHandling: "html",
    },
  );

  // translated html => md
  const mdx = await convertHtmlToMDX(translatedContent.text);

  return {
    content: mdx,
    // ...
  };
}

现在表单可以正确输入了!

Image.png

结束语

通过这次自动化实施,我们顺利完成了多语言版博文的部署过程。我将继续验证翻译后的文章是否按照我的预期进行了翻译,但现在,我期待着创建多语言的静态文件,并看看我所希望的搜索引擎优化改进是否真的带来了更多的流量!