📖

O'Reilly Online Learning BooksをDeepL Proでコードブロック以外を翻訳して読む

2023/06/25に公開

問題

DeepLでwebサイトを翻訳すると、以下のようなコードブロック[1]が、
before
以下のようにレイアウトが崩れたものになったり、意図しない部分が翻訳されたりすることがあります。
after

解決方法

以下のスクリプトを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での設定例↓
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 });

以上、参考になれば嬉しいです。

参考

https://torepico.com/programming/do-not-translate-code-block-by-deepl/
https://qiita.com/ishiyama0530/items/8f8afc6c36ca8ea4df56

脚注
  1. API Design Patterns, JJ Geewax より一部抜粋 ↩︎

Discussion