エンジニアリング//8 読了時間

Translation Sync Engineの内側:ローカライゼーションのための信頼性の高い非同期パイプラインをどのように構築したか

Eray Gündoğmuş
共有

翻訳管理は、3つのシステムを同期させようとするまでは簡単に聞こえます。開発者がコードを書くGitリポジトリ、翻訳者が作業するデータベース、そしてアプリが実行時に翻訳を取得するCDN。一方のシステムでキーを変更すれば、他の2つもそれを知る必要があります — 確実に、迅速に、そしてデータを失わずに。

これが、sync engineを構築して解決した問題です。この記事では、アーキテクチャ、メッセージタイプ、競合検出システム、そしてすべてを機能させる信頼性の保証について説明します。


同期翻訳ワークフローの問題

Better i18nの開発初期、翻訳の同期は同期的でした。開発者がコードをプッシュすると、webhookハンドラーが変更をインラインで処理し、データベースを更新し、CDNファイルを再生成して、レスポンスを返していました。うまく機能していました — うまく機能しなくなるまでは。

障害モードは予測可能でした:

  • タイムアウト。 5,000個のキーを持つリポジトリのdiffには時間がかかります。GitHubのwebhookには10秒のタイムアウトがあります。大規模なプロジェクトでは同期がサイレントに失敗していました。
  • 部分的な更新。 データベースの更新後にCDNのアップロードが失敗すると、翻訳が同期から外れました。誰かが手動で再同期をトリガーするまで、ユーザーは古いコンテンツを見続けました。
  • 可視性がない。 同期が失敗しても、何が起きたかの記録がありませんでした。デバッグにはサーバーログを読んでタイムスタンプを照合する必要がありました。

トリガーと処理を切り離し、自動リトライを提供し、すべての操作に完全な可視性を与えるアーキテクチャが必要でした。


Cloudflare Queuesの登場

sync engineのバックボーンとしてCloudflare Queuesを選択しました。Queuesは、まさに必要なものを提供します — 耐久性のある順序付きメッセージ配信と、少なくとも1回の配信セマンティクス。

アーキテクチャはシンプルです:

GitHub Webhook → API Handler → Queue (メッセージをエンキュー) → Worker (メッセージを処理)
                                                                        ↓
                                                       Activity Log + Database + CDN

APIハンドラーは最小限の作業をします:webhookを検証し、REPO_PUSH_SYNCメッセージをエンキューし、200を返します。実際の処理は、メッセージを受け取って実行するCloudflare WorkerであるQueueコンシューマーで非同期に行われます。

この分離には3つの即座のメリットがあります:

  1. Webhookのレスポンスが高速です。 大規模なリポジトリでもタイムアウトはなくなります。
  2. 障害は自動的にリトライされます。 Workerがクラッシュしたり、API呼び出しが失敗したりしても、メッセージは指数バックオフで再配信されます。
  3. 操作が観測可能です。 すべてのメッセージが構造化されたアクティビティログを生成します。

10のメッセージタイプ、1つのコンシューマー

sync engineは10種類のメッセージタイプを処理し、それぞれに専用のハンドラーがあります:

同期操作:

  • SYNC_START — 完全または増分的なGitHub同期。ファイルを取得し、キーを比較し、データベースを更新し、オプションで新しい翻訳のプルリクエストを生成します。
  • REPO_PUSH_SYNC — プッシュwebhookイベントの最適化パス。プッシュで変更されたファイルのみを処理するため、増分同期がほぼ即時になります。

CDN操作:

  • CDN_SETUP — プロジェクトがCDNを接続したときに、初期マニフェストと空の言語ファイルを作成します。
  • CDN_UPLOAD — R2ストレージに単一のJSON翻訳ファイルを書き込みます。
  • CDN_MERGE — 既存のCDNファイルに新しい翻訳をマージします。これは部分的な公開に重要です — 変更されていないものを削除せずに新しい翻訳を追加したい場合に使います。
  • CDN_CLEANUP — プロジェクトのすべてのR2ファイルを削除します。プロジェクトの削除時や、ユーザーが最初からやり直したいときに使用します。

AI操作:

  • AI_CONTEXT_ANALYSIS — Firecrawlを使用してプロジェクトのウェブサイトをスクレイピングし、そのコンテンツをGeminiに渡して翻訳コンテキストモデルを構築します。このコンテキストにより、機械翻訳が業界固有の専門用語を理解するのに役立ちます。
  • REPO_ANALYSIS — GitHubリポジトリをスキャンしてフレームワーク(React、Next.js、Flutterなど)を検出し、既存の翻訳を抽出し、用語集を構築します。

公開:

  • PUBLISH_BATCH — 翻訳ワークフローの最終ステップ。承認された翻訳を取得し、CDN(即時利用のため)とGitHub(バージョン管理のため)の両方にプッシュします。これはアトミック操作です — どちらかの書き込みが失敗した場合、公開全体がリトライされます。

用語集:

  • GLOSSARY_SYNC — 用語集をDeepLと同期します。「workspace」がフランス語では常に「espace de travail」と翻訳されるべきと定義すると、このメッセージによりDeepLの用語集が更新され、今後のすべての機械翻訳が一貫したものになります。

各メッセージタイプは分離されています。CDN_UPLOADの障害がSYNC_STARTをブロックすることはありません。遅いAI_CONTEXT_ANALYSISPUBLISH_BATCHを遅延させることもありません。この分離がエンジンの信頼性の鍵です。


ジョブシステム

メッセージは低レベルです。ジョブはユーザーやシステムが対話する高レベルのワークフローです。sync engineは12種類のジョブタイプをサポートします:

ジョブタイプトリガー生成されるメッセージ
initial_importプロジェクト設定SYNC_START, CDN_SETUP
incremental_syncプッシュwebhookREPO_PUSH_SYNC, CDN_MERGE
full_sync手動トリガーSYNC_START, CDN_UPLOAD (言語ごと)
source_syncソース言語の変更SYNC_START
bulk_translateバッチ翻訳リクエスト複数のCDN_UPLOAD
publish単一言語の公開PUBLISH_BATCH, CDN_UPLOAD
batch_publish多言語の公開複数のPUBLISH_BATCH
cdn_upload直接CDN書き込みCDN_UPLOAD
cdn_merge部分的なCDN更新CDN_MERGE
cdn_setupCDN初期化CDN_SETUP
cdn_cleanupプロジェクトクリーンアップCDN_CLEANUP
glossary_sync用語集の更新GLOSSARY_SYNC

1つのジョブが複数のメッセージを生成することがあります。例えば、8言語のプロジェクトでfull_syncジョブを実行すると、1つのSYNC_STARTメッセージと、各言語ファイルに対して8つのCDN_UPLOADメッセージが生成されます。ジョブはすべてのメッセージの集計ステータスを追跡します。


45以上のアクティビティアクション:構造化された可観測性

すべてのメッセージハンドラーは、処理が進むにつれて構造化されたアクティビティアクションをログに記録します。これらは自由形式のログ行ではなく、デバッグ体験とリアルタイムUIの両方を支えるタイプ付きの構造化されたイベントです。

典型的なSYNC_STARTフローでは、次のアクティビティトレイルが生成されます:

SYNC_STARTED
  → FETCH_FILES (GitHubから翻訳ファイルを取得中)
  → FILES_FETCHED (12ファイルが見つかりました)
  → COMPARE_KEYS (データベースとのdiff)
  → KEYS_ADDED (47の新しいキー)
  → KEYS_REMOVED (3つの非推奨キー)
  → KEYS_UPDATED (12の変更された値)
  → UPDATE_DATABASE (変更を永続化中)
  → PR_GENERATION_STARTED (翻訳PRを作成中)
  → PR_CREATED (PR #142がオープン)
  → SYNC_COMPLETED (所要時間: 3.2s)

45以上の異なるアクションタイプにより、すべての操作に粒度の細かい可視性が得られます。何かが失敗したとき、最後に記録されたアクションから、パイプラインがどこで停止し、どのデータがすでに処理されたかが正確にわかります。

これらのアクティビティアクションは、同期履歴UIも支えています。チームは、サーバーログに触れることなく、これまでに実行されたすべての同期、その内容、所要時間、成功したかどうかを確認できます。


競合の検出と解決

競合はあらゆる同期システムで最も難しい問題です。2人が同じ翻訳キーを編集する — 1人はコードベースで、もう1人は翻訳UIで。どちらが勝つのでしょうか?

私たちの答えは:自動的には誰も勝ちません。 sync engineは競合を検出し、人間が解決できるように表面化します。

検出

COMPARE_KEYSの間、エンジンは受信した各キーをデータベースと照合します。キーが最後の成功した同期以降にリポジトリとデータベースの両方で変更されていた場合、競合としてマークされます。エンジンは両方の値を変更タイムスタンプとともに保存します。

解決

競合はダッシュボードに完全なコンテキストとともに表示されます:

  • ソース値(リポジトリから)
  • データベース値(翻訳UIから)
  • 最後に同期された値(共通の祖先)
  • 各変更のタイムスタンプ

ユーザーは競合を1つずつまたは一括で解決でき、ソース値を保持するか、データベース値を保持するか、手動でマージするかを選択できます。すべての解決はアクティビティアクションとして記録されます。

このアプローチにより、翻訳ワークフローで最も一般的なデータ損失シナリオを防ぎます:開発者のコードプッシュが翻訳者の丁寧にレビューされた作業を黙って上書きするシナリオです。


信頼性の保証

sync engineは4つの信頼性原則に基づいて設計されています:

少なくとも1回の配信。 Cloudflare Queuesはすべてのメッセージが少なくとも1回配信されることを保証します。メッセージはWorkerの再起動、デプロイ、インフラの障害を乗り越えます。

べき等なハンドラー。 メッセージが複数回配信される可能性があるため、すべてのハンドラーはべき等です。同じコンテンツでCDN_UPLOADを再処理しても同じ結果が得られます。SYNC_STARTを再処理すると、現在のデータベースの状態と比較するため、重複した同期は実質的にno-opになります。

順序付き処理。 同じプロジェクトのメッセージは順序通りに処理されます。CDN_MERGEは常にそれを生成したSYNC_STARTの後に実行されます。これにより、データベースが新しいキーを反映する前にCDNファイルが更新されるレースコンディションを防ぎます。

バックオフによる自動リトライ。 失敗したメッセージは指数バックオフでリトライされます。一時的なエラー — APIレート制限、ネットワーク障害、一時的なR2の利用不可 — は人間の介入なしに自動的に解決されます。永続的なエラー(無効なデータ、権限の欠如)はログに記録され、ダッシュボードに表示されます。


チームにとっての意味

sync engineはバックグラウンドで動作します。GitHubリポジトリを接続すれば、同期は自動的に機能します。コードをプッシュすれば、翻訳は数秒以内に更新されます。翻訳を承認すれば、CDNに公開され、リポジトリにアトミックにコミットされます。

何かがうまくいかないとき — そして分散システムでは必ず何かがうまくいかなくなります — エンジンはリトライし、ログを記録し、問題を表面化します。サイレントな失敗はありません。一貫性のない状態もありません。翻訳の消失もありません。

それが、正しく行われた非同期処理の約束です:チームは翻訳に集中し、インフラが残りを処理します。

Comments

Loading comments...