「保存した覚えのない内容に、勝手に書き換わっている」という問い合わせを受けたことがあります。ノートアプリのようなリッチテキストエディタを持つプロダクトで、ユーザーが作ったテンプレートの一部の単語だけが、いつの間にか別の言葉になっていた、という内容でした。
結論から書くと、犯人はブラウザの「このページを翻訳しますか?」でした。アプリのバグではなかったのですが、アプリ側で防げる話でもあったので、調べたことをまとめておきます。
ページ翻訳はDOMそのものを書き換える
Chromeに内蔵されているGoogle翻訳のページ翻訳機能は、翻訳結果を表示用の別レイヤーに描くのではなく、実際のDOMのテキストノードを書き換えます。そして、その対象にcontentEditableな要素が含まれていても、特に区別しません。
リッチテキストエディタの実装は、だいたい以下のような形になっているはずです。
<div class="note-body" contenteditable="true">
<p>DOB: 1990/01/01</p>
<p>food allergy: なし</p>
</div>
このエディタの中身は、当然ながら「ユーザーが入力したデータそのもの」です。ところがページ翻訳が走ると、この<div>の中身も翻訳対象になります。DOBが生年月日に、foodが食べ物に置き換わった状態が、そのまま編集領域のDOMになるわけです。
なぜ「保存」まで到達してしまうのか
翻訳されただけなら、リロードすれば元に戻ります。問題はここからでした。
多くのエディタは、contentEditable要素の現在のHTMLを読み取って保存します。つまり、翻訳によって書き換わった後のDOMが、そのままユーザーのデータとして保存されうるということです。
しかもユーザー側の体験としては、こうなります。
- 英単語混じりのページを開いたら、Chromeが「翻訳しますか?」を出す(あるいは設定によっては自動で翻訳する)
- ユーザーは翻訳されたことに気づかないまま、まったく別の行を1文字だけ直す
- 保存する
- 触っていないはずの箇所まで日本語になっている
本人には「打ち替えた自覚」がまったくないので、「勝手にデータが変わった」という問い合わせになります。エディタ側のログを見ても、正規の保存リクエストが1本飛んでいるだけなので、アプリのバグとしては何も引っかかりません。ここが厄介なところでした。
書き換わったデータから翻訳を見分ける
すでに壊れてしまったデータについて、「翻訳が原因なのか、ユーザー自身の編集なのか」を切り分ける手がかりが2つあります。
1. <font>タグが新たに付いている
Google翻訳がDOMに文字列を挿入するとき、置き換えたテキストを<font style="vertical-align: inherit;">というタグで包むことが多くあります。dir="auto"が一緒に付くこともあります。
変更前後のHTMLを比較して、変化した箇所にだけこのタグが新規に付与されているなら、翻訳が原因である可能性が高いと判断できました。人間が普通にエディタで編集しても、こんなタグは生成されません。
2. 変換の内容が「正確すぎる直訳」になっている
DOB → 生年月日、food → 食べ物 のような、意味的に正しい英日変換が並んでいる場合も強い手がかりです。人間の打ち間違いや誤操作では、この組み合わせはまず発生しません。
この2つが揃った時点で、ほぼ翻訳と見て良さそうだ、という判断ができました。
対策:translate="no"を編集領域にだけ付ける
HTMLにはtranslateというグローバル属性があり、translate="no"を指定した要素とその配下は翻訳対象から外れます。MDNによればブラウザ自体がこの属性を解釈するわけではないものの、Google翻訳のような自動翻訳システムはこれを尊重するとされています。Baseline的にも2023年3月時点で主要ブラウザに行き渡っている属性です。
<div class="note-body" contenteditable="true" translate="no">
...
</div>
Googleは互換的にclass="notranslate"も認識するので、既存のクラスに足す形で併用しておいても構いません。
<div class="note-body notranslate" contenteditable="true" translate="no">
ページ全体を翻訳対象から外す方法もあり、<head>に以下を入れるとGoogleはそのページを翻訳しなくなります。
<meta name="google" content="notranslate">
ただし、これはやりすぎだと思います。UI自体の多言語対応(i18n)をしているアプリだと、「ブラウザ翻訳で読みたい」という正当なニーズまで潰してしまうためです。守りたいのはユーザーのデータであって、UIの文言ではありません。contentEditable要素、つまりデータ本体にだけtranslate="no"を絞って付けるのが、副作用が一番小さい落とし所だと考えています。
補助的に、<html lang="ja">のような言語宣言を正しく書いておくのも効きます。ブラウザの言語自動判定の精度が上がるので、「日本語のページなのに英単語が数個混じっていたせいで翻訳を提案された」という誤爆自体を減らせます。
対応するかどうかは別の判断
正直に書いておくと、私が関わったケースでは、このtranslate="no"の実装自体は最終的に見送りになりました。発生頻度と、エディタまわりに手を入れるリスクを天秤にかけた結果です。
ただ、原因が特定できていて対策も分かっている状態で「今はやらない」と決めるのと、原因不明のまま放置するのとでは、まったく意味が違います。問い合わせが来たときに「翻訳が原因の可能性が高いので、ページ翻訳をオフにしてお試しください」と即答できるだけでも、調査コストはかなり減りました。
まとめ
- Chromeのページ翻訳は、
contentEditable要素も区別せずDOMを直接書き換える - エディタがDOMを読んで保存する実装だと、翻訳結果がユーザーのデータとして保存されうる
- 壊れたデータは
<font style="vertical-align: inherit;">の有無と「正確な直訳」で切り分けられる - 対策は、編集領域にだけ
translate="no"を付けるのが安全
「アプリのコードは何も間違っていないのに、ブラウザの善意の機能がデータを壊す」というのは、なかなか想像しづらい経路でした。ユーザーの自由入力をHTMLとして保持しているプロダクトをお持ちの方は、一度自分のエディタ要素にtranslate="no"が付いているか確認してみると良いかもしれません。同じような問い合わせに悩んでいる方の参考になれば嬉しいです。