こんにちは、湖と月デザインです。
もともと日本語だけで運営していたサイトを、英語・ドイツ語にも対応させることになりました。最初はかなり単純に考えていました。日本語の文章を英語とドイツ語に翻訳して、言語切り替えを付ければいい。それだけの話だと思っていたんです。
ところが、実際に手を動かしてみるとそう簡単ではありませんでした。URL設計、翻訳データの管理、自動翻訳、固有名詞の誤訳、APIのレート制限、フォント、Google Analytics、Cookie同意、GeoIP、GDPR、ドイツ向けの法的要件。気がつけば「翻訳機能を追加する」という話ではなく、サイト全体の設計を見直すことになっていました。
今日は、JA/EN/DEの3言語対応を実際に進める中でやったことと、特に苦労したポイントをまとめてみます。
まずは、日本語前提のサイトを多言語対応にする
最初のサイトは、当然ながら日本語だけを前提に作られていました。そこに英語・ドイツ語を追加するにあたって、まずサイトのアーキテクチャそのものを「日本語サイト」から「多言語サイト」へ変更しました。

URLの構成は、パスプレフィックス方式(kototsuki.com/en/のような形)ではなく、サブドメイン方式を選びました。日本語は既存ドメインに残し、英語とドイツ語をサブドメインで分離する構成です。一見小さな変更に見えますが、実際には「現在の言語は何か」「別言語のページはどこにあるか」といった情報を、サイト全体で扱う必要が出てきます。文字列を3倍にするだけでは済まない部分です。
翻訳をどう管理するか
次に問題になったのが、翻訳そのものです。英語・ドイツ語のページをすべて手作業で作る方法もありますが、ブログ記事や事例のようなコンテンツが増えていくことを考えると、毎回3言語分を手で更新するのはかなり厳しい。そこで、DeepL APIを使ってビルド時に自動翻訳する仕組みを作りました。

翻訳対象には、UI文言、固定ページ、事例、ブログ記事などを含めています。これで、サイトのコンテンツを追加・変更すると、必要な翻訳が自動的に生成される仕組みになりました。
毎回全部翻訳するとAPIがもったいない
ここでもう一つ問題がありました。ビルドするたびに全コンテンツをDeepLへ送っていたら、変更していない記事まで毎回翻訳することになります。そこで、元テキストのハッシュを保存しておき、前回のハッシュと比較して、差分がある場合だけ再翻訳する仕組みにしました。

同じなら翻訳しない、違うならDeepLで再翻訳する、というだけの単純な分岐です。ブログ記事を1文字だけ修正した場合も、その記事だけが再翻訳されるので、不要なAPI利用をかなり減らせました。
DeepLの429エラーと向き合う
翻訳対象が増えてくると、今度はDeepL APIのレート制限に当たるようになりました。HTTP 429、つまりToo Many Requestsです。リトライ、指数バックオフ、翻訳済みデータの途中保存という3つの対策を入れましたが、特に効いたのは「途中保存」でした。

最初は、翻訳処理が最後まで正常に終わった場合だけキャッシュを書き出す構造になっていました。これだと、80件目で429が起きてビルドが失敗すると、それまで翻訳できていた80件分の結果も一緒に消えてしまいます。次のビルドでまた最初からやり直すことになるわけです。そこでtry/finallyを使い、途中でエラーが起きても、その時点まで翻訳できたデータをキャッシュへ保存するように変更しました。自動化するときは、正常に動いた場合だけでなく、途中で落ちたときに何が残るかまで考える必要がある、と実感した部分です。
機械翻訳で固有名詞が壊れる
自動翻訳を導入すると、別の問題も出てきます。サイト内の固有名詞である「湖と月デザイン」が、DeepLによって「Lake and Moon Design」のように翻訳されてしまいました。英文としては自然ですが、固有名詞なのでこれは困ります。
そこで、固有名詞をDeepLの翻訳対象から除外する仕組みを追加しました。<keep>湖と月デザイン</keep>のようにマークしておき、翻訳後に「湖と月デザイン」を「Kototsuki Design」へ置換する、という形です。

ここで学んだのは、機械翻訳を使う場合でも「全部自動」にする必要はない、ということでした。普通の文章は機械に任せて、固有名詞や重要な表現は人間が制御する。そういう仕組みにしておくほうが、結局は扱いやすいです。
そして、過去の誤訳を掃除する
この仕組みを入れたあと、さらに問題が発覚しました。過去のビルドで、すでに誤訳された状態のデータが翻訳キャッシュに残っていたのです。その数、52件。結局、誤訳済みのキャッシュを検出して削除し、正しいルールで再翻訳しました。自動生成されたデータは、コードを直したからといって、それだけで全部直るわけではありません。すでに生成済みのキャッシュや成果物が残っている場合は、そちらも含めてマイグレーションする必要があります。
翻訳したら、今度はUIが崩れた
翻訳がうまくいくようになると、今度は画面の問題が出てきました。特にドイツ語です。ドイツ語は日本語と比べて文章が長くなりやすく、パンくずリストの記事タイトルが長くなりすぎて、レイアウトが崩れてしまいました。そこで、表示上は32文字で切り、完全なタイトルはHTMLのtitle属性に残すようにしました。

多言語対応は「文字列を翻訳すること」ではなく、「翻訳後の文字列でもUIが成立するようにすること」なのだと、あらためて実感した出来事でした。
そして本題。ドイツ語版を追加したらGDPRとドイツの法律が出てきた
ここから話が少し変わります。ドイツ語版を追加することで、EU圏、とくにドイツからアクセスされる可能性が現実的に高くなりました。Google AnalyticsやCookieの扱いだけでなく、ドイツ国内でデジタルサービスを提供する場合の法的な表示義務なども意識する必要が出てきたのです。
ここで一度、関係する法律を分けて考える必要がありました。

名前が一緒に語られがちですが、役割はかなり違います。それぞれ何を指しているのか、ひとつずつ見ていきます。
GDPRとは
GDPR(General Data Protection Regulation・EU一般データ保護規則)は、個人データの処理やデータ主体の権利を扱う、EUの法律です。Google Analyticsのような外部サービスや、個人を識別できるデータの取り扱いが主な論点になります。
TDDDGとは
TDDDG(Telekommunikation-Digitale-Dienste-Datenschutz-Gesetz)は、Cookieなど端末への情報の保存・アクセスに関わる、ドイツの法律です。GDPRと隣接する領域ですが、論点は電子通信・端末データに特化しています。
DDGとは
DDG(Digitale-Dienste-Gesetz・デジタルサービス法)は、デジタルサービスを提供する事業者に、名称や連絡先といった情報提供を求める、ドイツ国内のルールです。DDGの第5条では、対象となるデジタルサービスについて、名称・所在地・連絡先などを容易に認識でき、直接アクセスできる状態で継続的に提供することが定められています。
便宜上「GDPR/DDG対応」とまとめて話すこともありますが、実際にはそれぞれ別の論点として、ひとつずつ検討していきました。
GDPR対応は「どこから来たか」で判定する
GDPR側でまず問題になったのが、Google AnalyticsとCookieです。最初に思いついたのは「ドイツ語ページだからCookieバナーを出す」というやり方でした。でも、これだと少しおかしなことになります。ドイツから日本語ページを見ることもできますし、日本からドイツ語ページを見ることもできるからです。
そこで、判定基準を「ページの言語」ではなく「訪問者の所在地」に統一しました。if language == "de"ではなく、if visitor_country == "JP"という考え方です。これなら、日本からドイツ語ページを見る場合も、ドイツから日本語ページを見る場合も、同じ基準で扱えます。

図にしてみると、言語だけで判定すると、必要な場面でバナーが出ない、あるいは必要ない場面でバナーが出てしまう、というズレが起きることがわかります。訪問者の所在地を基準にすることで、このズレを解消しました。
訪問者の国を判定するために、既存のprivate-analytics-appサーバーにgeoip-liteを導入し、/geoというエンドポイントを新設しました。geoip-liteはローカルのデータベースを利用するので、外部のGeoIP APIへ問い合わせる必要がありません。
この判定は、Analyticsだけでなく、Google Fonts、お問い合わせフォーム、プライバシーポリシーの表示にまで関わってきます。整理すると、こういう形になりました。
ひとつのGeoIP判定が、4つのまったく別の実装につながっている。図にしてみると、思っていたより広い範囲に影響していたことがわかります。ただし、これはあくまで今回のサイトが採用した設計です。「日本なら必ず同意なしでAnalyticsを使える」という意味ではなく、実際の適用関係は、利用するサービスや処理内容、事業者の状況によって判断が変わってきます。
DDG側で考えたこと。「海外向けサービス」に見えないようにする
GDPR対応とは別に、ドイツ語サイトを公開することで、ドイツから見たときにこのサイトがどう見えるか、という点も考える必要がありました。今回のサービスは日本国内向けです。それにもかかわらず、日本語・英語・ドイツ語の3言語でサービス内容を説明し、さらに価格プランまで提示すると、海外、とくにEU圏のユーザーも対象にサービスを提供しているように見える可能性があります。
そこで、単純に翻訳するだけではなく、誰を対象としたサービスなのかが明確になるよう、表現そのものも調整しました。これはDDGそのものが「ドイツ語サイトならプランを隠せ」と要求しているわけではありません。ドイツ向けのデジタルサービスとして受け取られる余地を、サイト設計側からできるだけ減らしておく、という考え方です。
トップページの対応エリアについても、DeepLの自動翻訳に完全には任せませんでした。たとえば「全国対応」という表現は、ドイツ語として自然に訳すだけでなく、日本国内を対象としていることが明確に伝わる言い方に、手作業で調整しています。加えて、本サービスは日本国内向けで、海外のお客さまを対象としたものではない、という趣旨の注記も追加しました。ドイツ語で読めることと、ドイツ市場をターゲットにしていることは別だ、という点を、文言のひとつひとつで意識した部分です。
最終的にどうなったか

最終的な構成をざっくりまとめると、上のような形になりました。単純な3言語サイトのつもりが、多言語URL、翻訳パイプライン、翻訳キャッシュ、差分検知、APIリトライ、固有名詞制御、UIの多言語対応、GeoIP、Cookie同意、Analytics制御、フォントの自己ホスト、地域別コンテンツ、プライバシーポリシーの出し分け、ドイツ向けの事業者情報・サービス表示の見直しまで含む構成になっていました。
一番大変だったのは、実は「翻訳」ではなかった
今回やってみて一番感じたのは、多言語対応は翻訳機能を追加することではない、ということです。最初は日本語を英語・ドイツ語に翻訳するくらいの話だと思っていました。実際には、影響範囲がどんどん広がっていきました。

特に印象に残っているのは、「ドイツ語版を追加する」という技術的な変更が、最終的には「このサイトは誰を対象にしているのか」という事業・法務の話につながったことです。
多言語サイトを作って分かったこと
今回の対応で得た教訓を、いくつかまとめておきます。
- 最初からN言語を前提にする — ja、en、deをそれぞれ個別に実装するのではなく、最初からN言語として扱える設計にしておく。言語が3つから4つ、5つに増えたときに、また作り直すことにならないようにするためです。
- 機械翻訳は「完全自動」にしない — DeepLはかなり便利ですが、固有名詞まで正しく扱ってくれるとは限りません。サービス名、会社名、法的な表現、国や地域に関する表現は、人間が制御できる仕組みを残しておくのが大事でした。
- 翻訳後のUIを必ず見る — 日本語で収まっていた文字列が、英語やドイツ語では普通に溢れます。翻訳が正しいかだけでなく、翻訳された状態でもUIが壊れないかまで見る必要があります。
- GDPR対応を「ドイツ語ページかどうか」で考えない — 今回の実装では、ページの言語ではなく訪問者の所在地を基準にしました。日本語ページをドイツから見る人もいれば、ドイツ語ページを日本から見る人もいるからです。
- GDPR・TDDDG・DDGを一括りにしない — 「ドイツ向けのサイトだからGDPR対応」という一言では、実際に何を対応しているのか分かりません。個人データの保護、端末への保存・アクセス、事業者の情報提供義務。それぞれ論点が違うので、この対応はどの法律・どの要件を意識しているのかを分けて考えたほうが、実装も整理しやすくなります。
- 法務と技術は意外と近い — 最終的には、どんな言語で表示するかだけでなく、誰をサービス対象としているように見せるかまで考えることになりました。多言語サイトでは、技術的な実装とサイト上の表現、そして法的な要件がかなり密接につながっています。
多言語対応とGDPRについて、よくある質問
Q. 多言語サイトを作ると、GDPR対応は必須になりますか?
言語を追加すること自体が対応を義務付けるわけではありません。ただ、ドイツ語やフランス語など、EU圏で使われる言語を追加すると、EUからのアクセスが増える可能性が高くなります。個人データを扱う仕組み(Analyticsや外部サービス)がある場合は、訪問者の所在地に応じた対応を検討する必要が出てきます。
Q. サブドメイン方式とパスプレフィックス方式、どちらがSEOに向いていますか?
どちらも運用実績のある方式です。サブドメイン(en.example.com)は言語ごとにサーバー設定を分けやすく、パスプレフィックス(example.com/en/)はドメインの評価を1つに集約しやすい、という違いがあります。今回はサーバー構成のシンプルさを優先してサブドメイン方式を選びました。
Q. DeepL APIのレート制限(429エラー)はどう回避すればいいですか?
リトライと指数バックオフに加えて、処理が途中で失敗しても、その時点までの翻訳結果をキャッシュに保存する仕組みが有効でした。翻訳量が多いサイトほど、429エラーは前提として設計に組み込んでおいたほうが安全です。
Q. GDPRとDDG、TDDDGは、それぞれ別に対応が必要ですか?
論点が異なるので、まとめて「1つの対応」として考えるより、個人データの取り扱い(GDPR)、Cookieなど端末データの取り扱い(TDDDG)、事業者情報の提供義務(DDG)を分けて確認したほうが、実装の抜け漏れを防ぎやすくなります。
Q. 訪問者の所在地判定(GeoIP)は、精度が心配です。どう運用していますか?
今回は、判定できた場合は所在地に応じて処理を分け、判定できなかった場合は安全側に倒してCookie同意を必須にする、という設計にしました。GeoIPの判定精度に完全には依存しない構成にしておくのがポイントです。
まとめ
今回の多言語化は、最初こそ「英語とドイツ語を追加する」という小さな変更でした。しかし、実際にはサイト全体を見直すきっかけになりました。
翻訳についてはDeepL APIで自動化し、差分検知やキャッシュ、リトライを組み合わせて運用できるようにしました。一方で、固有名詞や重要な表現は自動翻訳に任せず、個別に制御する仕組みも必要でした。そしてドイツ語版の追加をきっかけに、GDPRを意識したCookie同意、GeoIPによる地域判定、Google Fontsの自己ホスト、地域別のコンテンツ表示にも対応しました。さらに、GDPRだけでなく、ドイツのデジタルサービスに関するDDGや、Cookieに関係するTDDDGについても、それぞれ別の論点として考える必要がありました。
結果として、「サイトを3言語にする」ではなく、「3言語で運用できるサイトに作り変える」という作業になりました。多言語対応をこれから始める方にとって、少しでも参考になればと思います。

