Google翻訳とGrammarlyを1つのアプリにして英作文セルフレビューを効率化する

オフショア先とやりとりをしたり英語でコメントやでコミットログを書くときは、自信がないのでGrammarlyでの文法チェックとGoogle翻訳での対訳でセルレビューをする習慣をつけています。
 
- 参考： [英語校正ツールでここまで感動したことはあっただろうか？　GitHubでの英文クオリティを劇的に改善！Grammarly - Qiita](https://qiita.com/07JP27/items/91ed401989b3c796806f)

とはいえ、Webブラウザで毎回GrammarlyとGoogle翻訳を開いたりタブを探したりするのが面倒くさくなっていました。

## 1画面に2つのWebサービスをまとめたい

どうせ同じ英文を両方のサービスにペーストするなら、1つの画面で両方いい感じに開けて、ブラウザとは別のアプリケーションとしてまとまっていたら便利そうです。（下図）
![上にはGrammarlyを表示する 英文をサクサクコピペして添削と対訳をチェックできる! 下は高さ固定領域にGoogle翻訳を表示 ツールバーとかはいい感じに非表示にする](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48238/29dc3ac2-a5ca-99a0-d744-60bb7743f444.png)
真っ先に考えたのはiframeを使って複数のWebサイトを1HTML内に埋め込むことでしたが、いずれのサービスもiframeからの使用は禁止されていました。

だったらElectronでできるんじゃないかということで、作ってみることにしました。

(開発環境は macOS Mojave、Node.js 12.2.0、Electron 7.1.1 です。)

## 今のElectronは1つのウィンドウに複数のブラウザを表示できる

最近のElectronのAPIでは、Experimentalですが複数のWebページの表示を同時に配置できます。[昔いじったとき](https://media-massage.net/works/docs/my_first_electron_app_for_pdf_comparison/)はそんな機能なかったので、GitHubを調べてみました。どうやら2018年の12月頃に実装されたみたいです。

- [feat: add multi BrowserView support to BrowserWindow by summeroff · Pull Request #16148 · electron/electron · GitHub](https://github.com/electron/electron/pull/16148)

ひとつのBrowserWindowの中に、複数のBrowserViewを表示させることができます。BrowserViewの中はブラウザのタブのようにそれぞれ他と独立して動いているので、関係ないURLを1画面に表示することも問題ありません。これならやりたいことができますね！

## JavaScriptでラフに実装

それでは実装してみます。まずは適当なディレクトリ内に`package.json`を作成します。

```shell
mkdir qiita-test
cd qiita-test
npm init -y
npm i -D electron
touch index.js
```

この`index.js`はElectronのメインプロセスの実装になります。今回のアプリは既存のWebサイトを表示するだけなので、作るのはこれだけです！

とりあえずゴリゴリ書いたのが以下のコードになります。

```js:index.js
const { app, BrowserView, BrowserWindow, screen } = require('electron');

app.on('ready', () => {

  // デスクトップ領域の大きさを取得
  const workArea = screen.getPrimaryDisplay().workArea;
  
  // 推奨サイズ
  const initialWidth = 1300;
  const initialHeight = 940;

  // BrowserWindowを新規作成
  let win = new BrowserWindow({ 
    width: Math.min(workArea.width, initialWidth),
    height: Math.min(workArea.height, initialHeight),
  });

  // BrowserWindowを閉じたらアプリ終了
  win.on('closed', () => {
    win = null;
    app.quit();
  });

 // レイアウト指定用の定数
 const { width, height } = win.getBounds();
 const gTransHeight = 300;

 // Grammaly用のBrowserViewを作成
  const grammarly = new BrowserView({
    webPreferences: {
      // nodeの機能を無効化
      nodeIntegration: false,
    }
  });

  // BrowserWindowに追加
  win.addBrowserView(grammarly);
  
  // 配置する位置とサイズを指定
  grammarly.setBounds({
    x: 0,
    y: 0,
    width: width,
    height: height - gTransHeight
  });
  
  // URLを開く
  grammarly.webContents.loadURL('https://www.grammarly.com/signin?allowUtmParams=true');

 // Google翻訳画面用に同じことをする…
 
  const gTransView = new BrowserView({
    webPreferences: {
      nodeIntegration: false,
    }
  });
  win.addBrowserView(gTransView);
  gTransView.setBounds({x: 0, y: height - gTransHeight, width: width, height: gTransHeight });
  gTransView.webContents.loadURL('https://translate.google.com/');

  // Google翻訳画面にCSSを挿入してツールバーを隠す
  gTransView.webContents.on("dom-ready", () => { 
    gTransView.webContents.insertCSS(`
      /* ヘッダー隠す */
      body > header { display: none !important; }
      /* 境界線入れる */
      .frame { border-top: 3px solid black !important; padding-bottom: 66px !important; padding-top: 15px !important; }
      /* 使わないボタンを隠す */
      .input-button-container { display: none !important; }
    `);
  });

  // リサイズ時に配置位置を再計算
  win.on('resize', () => {
    const { width, height } = win.getBounds();
    grammarly.setBounds({x: 0, y: 0, width: width, height: height - gTransHeight });
    gTransView.setBounds({x: 0, y: height - gTransHeight, width: width, height: gTransHeight });
  });

});
```
### 補足・解説

#### BrowserViewの自動リサイズはうまく動かない

BrowserViewには`setAutoResize()`というメソッドがあり、これを使うとBrowserWindowの伸縮に合わせてBrowserViewの大きさも自動的に変えてくれる機能があります。

https://electronjs.org/docs/api/browser-view#viewsetautoresizeoptions-experimental

しかし、この機能はまだExperimentalで、今回の複数のBrowserViewを配置する使い方ではレイアウトが崩れるケースもありました。このため、`setAutoResize()`は使わずに、ウィンドウリサイズ時にはそれぞれのBrowserViewで`setBounds()`をやり直す実装にしています。

```ts
  // リサイズ時に配置位置を再計算
  win.on('resize', () => {
    const { width, height } = win.getBounds();
    grammarly.setBounds({x: 0, y: 0, width: width, height: height - gTransHeight });
    gTransView.setBounds({x: 0, y: height - gTransHeight, width: width, height: gTransHeight });
  });
```

#### 表示したWebサイトのCSS上書き

`webContents`の`insertCSS()`で、CSSを自由に追加することができます。

https://electronjs.org/docs/api/web-contents#contentsinsertcsscss-options

ただし、`insertCSS()`を実行する前にWebサイトの読み込みを待つ必要があるので、
今回の例では`dom-ready`イベントで実行しています。

```ts
  // Google翻訳画面にCSSを挿入してツールバーを隠す
  gTransView.webContents.on("dom-ready", () => { 
    gTransView.webContents.insertCSS(`
      /* ヘッダー隠す */
      body > header { display: none !important; }
      /* 境界線入れる */
      .frame { border-top: 3px solid black !important; padding-bottom: 66px !important; padding-top: 15px !important; }
      /* 使わないボタンを隠す */
      .input-button-container { display: none !important; }
    `);
  });
```

## 作ったアプリを起動する

とりあえずアプリを起動するにはこのコマンドを叩きます。

```shell
npx electron .
# ctrl + c で終了
```
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48238/a4933eba-b0b8-9a60-434a-1b76b8722072.png)
起動しました! 目に見えるアプリができるとテンション上がりますね。
Grammarlyは一度ログインしてしまえば、次回はログイン後の画面から立ち上がるようになります。

## electron-packagerでのパッケージング

このままだと毎回コマンドを叩かないと起動してくれないので、macOSアプリとしてパッケージ化します。パッケージ化するためのツールもいくつかの種類がありますが、今回は[electron-packager](https://github.com/electron/electron-packager)というツールを使ってみます。

```shell
npm i -D electron-packager
npx electron-packager . QiitaTest --platform=darwin --arch=x64 --electron-version=7.1.1 --out=./build --asar
```
これで、QiitaTestという名前のアプリケーションが生成されました！ ダブルクリックすれば本当にアプリとして使うことができます。
(ただし、本格的に配布するには署名が必要です。この記事では立ち入りません……。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48238/25ff6a06-9a31-8d8c-a6f0-93aae4182149.png)

## TypeScriptで少し丁寧に作り直す

このあとTypeScriptを導入してもうちょっと綺麗に作り直しましたが、アプリの動きは全く同じなので割愛します。
https://github.com/hokkey/translators-web

## 単純だけど便利

作ったアプリはなかなかの便利さで、業務でも重宝しています。

GrammarlyとGoogle翻訳の組み合わせ以外にも、

 - Slack, Redmine, GitLabなど、ある1つの案件に絡むWebサービスを1アプリにまとめて表示する
 - よく使う辞書サイトを1つのアプリにまとめる

など、アイデア次第でほかにも応用できそうです！