1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chromeのページ翻訳が、リッチテキストエディタの中身を書き換えていた話

1
Posted at

「保存した覚えのない内容に、勝手に書き換わっている」という問い合わせを受けたことがあります。ノートアプリのようなリッチテキストエディタを持つプロダクトで、ユーザーが作ったテンプレートの一部の単語だけが、いつの間にか別の言葉になっていた、という内容でした。

結論から書くと、犯人はブラウザの「このページを翻訳しますか?」でした。アプリのバグではなかったのですが、アプリ側で防げる話でもあったので、調べたことをまとめておきます。

ページ翻訳は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が、そのままユーザーのデータとして保存されうるということです。

しかもユーザー側の体験としては、こうなります。

  1. 英単語混じりのページを開いたら、Chromeが「翻訳しますか?」を出す(あるいは設定によっては自動で翻訳する)
  2. ユーザーは翻訳されたことに気づかないまま、まったく別の行を1文字だけ直す
  3. 保存する
  4. 触っていないはずの箇所まで日本語になっている

本人には「打ち替えた自覚」がまったくないので、「勝手にデータが変わった」という問い合わせになります。エディタ側のログを見ても、正規の保存リクエストが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"が付いているか確認してみると良いかもしれません。同じような問い合わせに悩んでいる方の参考になれば嬉しいです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?