O'Reilly Online Learning BooksをDeepL Proでコードブロック以外を翻訳して読む
問題
DeepLでwebサイトを翻訳すると、以下のようなコードブロック[1]が、
以下のようにレイアウトが崩れたものになったり、意図しない部分が翻訳されたりすることがあります。
解決方法
以下のスクリプトをdeveloper toolsのconsoleから実行することで、O'Reilly Online Learningのような動的サイトにおいても、コードブロックの翻訳を防ぐことができます。
const observer = new MutationObserver(() => {
document.querySelectorAll('pre').forEach(elem => elem.classList.add('notranslate'))
});
observer.observe(document, { childList: true, subtree: true });
ScriptAutoRunner等のスクリプトランナーツールに登録しておくと、webページを開いた際に自動実行してくれて、とても便利なのでおすすめです。
ScriptAutoRunnerでの設定例↓
解説
前提として、DeepLではDisable Translation of Elementsに記載されるように、HTML要素にtranslate="no"
もしくはclass="notranslate"
という属性が設定されていると、その要素の翻訳はスキップされるようになっています。
であれば、コードブロック(pre
要素)の翻訳を防ぐためには、ScriptAutoRunner等で全てのpre
タグにclass="notranslate"
を追加するような以下のスクリプトを実行すれば良さそうで、実際、静的サイトではこちらで上手くいきます。
document.querySelectorAll('pre').forEach(elem => elem.classList.add('notranslate'));
しかし、O'Reilly Online Learningのように動的にコンテンツを生成するようなサイトでは上手く行きません。
その理由は主に2つあります。
1. スクリプト実行時にはまだ動的コンテンツができていない(ことがある)から。
ScriptAutoRunnerでは、content script実行時に(sendMessageを通して)ユーザが登録したスニペットやライブラリを実行する作りになっており、content scriptの実行タイミングはデフォルトのdocument_idle
です。
Content scripts#run_timeに示されるように、document_idle
だと実行タイミングは遅くてもwindow.onload
イベントが発火した直後であり、window.onload
イベントの発火時点では、まだ動的に生成されるDOM要素が作られていないことがあります。そのため、そこに含まれるpre要素には対応できないことがあります。
2. 動的コンテンツの変更に対応できないから。
スクリプトが実行されるのはページload時の一度きりなので、ページ遷移せずに内部のコンテンツが切り替わる場合には、当然content scriptが再度読み込まれることはないため、新たに追加されたpre要素には対応できません。(変更の度に手動でconsoleから実行する手もありますが、それはあまりに面倒なので避けたいところです。)
以上を踏まえると、動的コンテンツの変更を検知してclass属性の追加処理を走らせることができれば良さそうです。これは、MutationObserverを用いて実現できます。document以下のDOM全体の変更を監視し、class="notranslate"
の追加処理をコールバック関数として渡すことで上手くいきます。
const observer = new MutationObserver(() =>
document.querySelectorAll('pre').forEach(elem => elem.classList.add('notranslate'))
);
observer.observe(document, { childList: true, subtree: true });
パフォーマンスに関しては、実際にコールバックが発火するのはO'Reilly Booksの場合1ページあたりせいぜい数十回程度のようだったのと、処理が非常に軽い(手元のM1 Macで約1000ノードあるDOMで1回あたり約1e-2 ms)こともあり、(使用感的にも)全く気にするほどではなさそうでした。
(以下はほぼ興味本位の思考実験ですが、)
もし極端にDOMの変更頻度が高くパフォーマンス悪化が気になるような場面があれば、以下のようにsetTimeout
を用いたりすることで、適度に処理実行の間隔を空けることもできそうです。
let runnable = true;
const observer = new MutationObserver(() => {
if (runnable) {
runnable = false;
setTimeout(() => {
runnable = true;
document.querySelectorAll('pre').forEach(elem => elem.classList.add('notranslate'));
}, 500);
}
});
observer.observe(document, { childList: true, subtree: true });
もしくは、もし極端にノード数が多いような場面があれば、実際には追加されたノードのみチェックすれば十分なので、走査対象のノードをdocument全体ではなく、以下のようにMutationRecord
オブジェクトのaddedNodes
以下に限定して改善を図ることもできるかもしれません(一応動作確認済み)。
document.querySelectorAll('pre').forEach(elem => elem.classList.add('notranslate')); // 生成済みのpre要素のために必要
const observer = new MutationObserver(mutationList => {
mutationList.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
node.querySelectorAll('pre').forEach(elem => elem.classList.add('notranslate'))
})
})
})
observer.observe(document, { childList: true, subtree: true });
最後に、個人的にはtable要素も翻訳しない方が読みやすいことが多いので、実際には以下のコードを用いていたりします。
const observer = new MutationObserver(() => {
document.querySelectorAll(['table', 'pre']).forEach(elem => elem.classList.add('notranslate'))
});
observer.observe(document, { childList: true, subtree: true });
以上、参考になれば嬉しいです。
参考
-
API Design Patterns, JJ Geewax より一部抜粋 ↩︎
Discussion