DeepL APIを活用した多言語ブログの自動化を実現

この記事は DeepL によって翻訳されました。誤訳があれば教えてください!

背景

技術ブログの運営において、コンテンツの到達率は主要な考慮事項である。特に、韓国語のみで作成されたコンテンツは、その範囲が限定的であり、これは全世界の開発者との知識共有を制限する要素として作用する。実際の開発者人口統計によると、韓国は上位15カ国中、下位に属しており、多言語サポートの必要性がさらに浮き彫りになる。

Image.png

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

自動化の必要性

最近のAI翻訳や伝統的な翻訳ツールは、かなり正確な翻訳を提供しています。しかし、新しい記事を作成するたびに手動で翻訳してアップロードする作業は非効率的である。特に、英語だけでなく、開発者数が多い中国語、日本語版まで提供したい場合、手作業では現実的に不可能です。

このような問題を解決するために 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

そして、次のようにscriptを生成しました。

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のscriptも追加します。

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

実行結果

ターミナルでコマンドを入力したら

Image.png

翻訳されたmdxファイルが生成されます。

Image.png

問題点

初期の実装では、MDXファイルのテキストを直接DeepL APIに転送する方式を採用したが、次のような問題が発見された:

  1. マークダウンの文法が損なわれる
  2. コードブロックの不要な翻訳
  3. 画像タグとリンク構造の歪み
  • オリジナル

Image.png

  • 日本語翻訳版

Image.png

解決方法

どうしようかと悩んでいたところ、次のようなアイデアが浮かびました。まず、DeepL APIのドキュメントでHTML handlingというパートを見たところ、テキストをHTMLで送るとフォームを壊さずにうまく処理してくれるようでした。

したがって、上記の問題を解決するために次のような改善されたプロセスを導入しました。

  1. MDX → HTML 変換
  2. HTMLタグ保存オプションを活用した DeepL API送信
  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

おわりに

今回の自動化実装で簡単にブログポストの多言語版配布プロセスが完成しました。翻訳された投稿が私が意図した通りに翻訳されたかどうかは引き続き検証してみなければならないが、まずは複数の言語で静的ファイルを作ってSEO向上が期待される分、実際に流入が増えるかどうか見てみよう!