ノムリッシュ翻訳ライブラリをnpmに公開しました

# リンク
[npm](https://www.npmjs.com/package/nomlish)
[github](https://github.com/SugarShootingStar/nomlish)
[ノムリッシュ翻訳サイト](https://racing-lagoon.info/nomu/translate.php)
[ニコニコ大百科](http://dic.nicovideo.jp/a/%E3%83%8E%E3%83%A0%E3%83%AA%E3%83%83%E3%82%B7%E3%83%A5)

# まえがき
ノムリッシュ翻訳サイトに

```
当サイトを利用して動画・小説・同人誌などのコンテンツを作成される皆さまへ
動画説明文やあとがき等にURL(http://racing-lagoon.info/)を記載頂けると助かります。
また、よく転載に関するお問合せを頂きますが、特にご連絡は不要ですのでご自由にお使いください
```
と書いてあったのでライブラリを作ってみました。

# ノムリッシュ翻訳とは
ニコニコ動画などで流行っているノムリッシュ◯◯の元ネタにもなっている、日本語をファイナルファンタジー風に翻訳してくれるサービスのことです。

例えば、この記事のタイトルでもある「ノムリッシュ翻訳ライブラリをnpmに公開しました」を翻訳すると

```
ノムティスリッシュオナフェルベルクスヤーンバッカースフェルチ化ライブラリの更に上を行く黒き不滅の刃をnpmにパージしました
```

となります。(？？？)

# ライブラリの使用方法

```
npm install nomlish
```

か

```
yarn add nomlish
```
すればインストールは終了です。

READMEにも記載されていますが、こんな感じで変換できます。

- 翻訳(翻訳レベル指定なし)

```javascript
const nomlish = require("nomlish");

const beforeText = "任意の文字列をノムリッシュ・テキストに変換するライブラリです。";

nomlish.translate(beforeText)
  .then((nomlishText) => {
     // ニン・インの文字列をノムリスッシュ――テキストに変換実行するラインヴラ・リ=フォースライトだと願うことは、許されなかった──。
    console.log(nomlishText);
  });
```

- 翻訳(翻訳レベル指定あり)

```javascript
const nomlish = require("nomlish");

const beforeText = "任意の文字列をノムリッシュ・テキストに変換するライブラリです。";
const level = 4;

nomlish.translate(beforeText, level)
  .then((nomlishText) => {
     // ・・・そのグルガン族の男は静かに語った・・・善悪の彼岸に立ちそれを望む者任意のビビとお医者さんごっこをした文字列をノムティスリスッ・シェュ(CV・日野聡)……神代文字に破壊と創造宿命を背負う騎士千人分の魔力を誇るラーインヴラー＝リかもしれぬな…。・・・これは、一篇の物語ではない。自ら綴る歴史であり、運命である。
    console.log(nomlishText);
  });
```

# ライブラリの実装について

axiosを使ってノムリッシュ翻訳サイトに行き、取得したHTMLをlibxmljsでパースします。その後tokenをpostパラメータに含めPOST，その後またHTMLをパースし翻訳後テキストを取得して返します。
当初はcheerioを使っていたのですが、libxmljsの方が高速に動作するとのことだったのでこちらを選択しました。

また、axiosに関しても当初はrequestを使っていたのですが[axiosでもjarを使えることがわかったのでaxiosを使うようにしました。](https://qiita.com/SugarShootingStar/items/63744a8f4237862477c0)

(2018/10/1追記) POSTする時にURLSearchParams()を使って実装していたのですが、nodeのバージョンが10でないとそのまま呼べずnode8だとurlをrequireしてこなければなりませんでした。
それでaxiosのドキュメントを見た所、nodeではquerystringを使った実装でも問題なく動作させることができるとのことだったのでquerystringを使った実装に変更しました。今では6,8,10のどれでも動作を確認できています。

```javascript:src/main.js
let axios = require('axios').default;
const libxmljs = require("libxmljs");
const querystring = require('querystring');
const axiosCookieJarSupport = require('axios-cookiejar-support').default;
const tough = require('tough-cookie');
const cookieJar = new tough.CookieJar();
axiosCookieJarSupport(axios);
axios.defaults.withCredentials = true;
axios.defaults.jar = cookieJar;

const NOMLISH_URL = "https://racing-lagoon.info/nomu/translate.php";
const xpathToekn = '//input[@name="token"]';
const xpathAfter = '//textarea[@name="after1"]';

/**
 * テキストをノムリッシュテキストに変換します。
 *
 * @export
 * @param {String} text 変換前テキスト
 * @param {Number} level 翻訳レベル:1~6
 * @returns ノムリッシュ・テキスト
 */
export function translate(text, level) {
  return new Promise((resolve, reject) => {
    axios.get(NOMLISH_URL)
      .then((response) => {
        let html = libxmljs.parseHtml(response.data);
        // ページを開いた時にhidden要素で用意されているtokenを入れる
        const token = html.get(xpathToekn).attr("value").value();
        const postParams = setPostParam(text, level, token);
        
        axios.post(NOMLISH_URL, postParams)
          .then((response) => {
            html = libxmljs.parseHtml(response.data);
            return html.get(xpathAfter).text();
          })
          .then(nomlishText => resolve(nomlishText))
          .catch(error => reject(error.response.status));
      })
      .catch(error => reject(error.response.status));
  });
}


/**
 * 翻訳レベルを取得する
 *
 * @param {Number} level 翻訳レベル
 * @returns 2~5の翻訳レベル
 */
function getLevel(level) {
  if(0 < level && 6 > level) {
    return level;
  } else {
    return 2;
  }
}

/**
 * フォームをPOSTするのに必要なパラメータの設定
 *
 * @param {String} text 変換前テキスト
 * @param {Number} level 翻訳レベル:1~6
 * @param {String} token POSTに必要なトークン
 * @returns POSTパラメータ
 */
function setPostParam(text, level, token) {
  const params = {
    options : "nochk",
    transbtn: "翻訳",
    before  : text,
    level   : getLevel(level),
    token   : token
  };
  return querystring.stringify(params);
}
```

# テストについて

テストは初めてでよくわからなかったのですがavaを使いました。すごい簡単に書けてテストできたのでびっくりしています。ソースの量が少ないのでこんな感じのテストコードでカバレッジ100%でした。

```javascript:test/test.js
const test = require("ava");
const nomlish = require("../dist/main");
const beforeText = "テスト用文字列あいうえおシュガーシューティングスターフェットチーネグミピュレグミイエーイ渋谷最高";

test('textOnly', t => {
  return nomlish.translate(beforeText).then((nomlishText) => {
    t.not(nomlishText, beforeText);
  });
});

test('level-string', t => {
  const level = "4";
  return nomlish.translate(beforeText, level).then((nomlishText) => {
    t.not(nomlishText, beforeText);
  });
});

test('level-number', t => {
  const level = 4;
  return nomlish.translate(beforeText, level).then((nomlishText) => {
    t.not(nomlishText, beforeText);
  });
});

// 翻訳レベルに不適切なパラメータが入った場合、デフォルトの翻訳レベル2で翻訳が実行される
test('level-incorrect', t => {
  const level = "不適切なレベル";
  return nomlish.translate(beforeText, level).then((nomlishText) => {
    t.not(nomlishText, beforeText);
  });
});
```

# travis CIおよびCoverallsの設定

どちらも初めて使うサービスでしたがググればわりと簡単に使えました。
各サービスのサイトに行きGithubのリポジトリを認証、動作させるように設定した後設定ファイルを書きpushするだけです。
travisではnodeのバージョンが6,8,10の時のテストをしています。
カバレッジ取得にはnycを使用しました。avaのテストコマンドの一つ前に付けただけで動いたので楽でした。

```yml:.travis.yml
language: node_js
node_js:
  - 10
  - 8
  - 6
script:
  - npm run test

after_success:
- './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls'
```

```yml:.coveralls.yml
service_name: travis-ci
```

# ライブラリのビルド方法やトランスパイル方法について
`@babel/core`、`@babel/preset-env`、`babel-loader`、`webpack`、`webpack-cli`、`webpack-node-externals`を使用しました。
webpackのビルドは開発と本番で設定ファイルを分けています。
開発用の設定ファイルではカバレッジ取得のため、ビルドファイルにソースマップを出力しています。
本番用の設定ファイルでは容量削減のためソースマップを含めずminimizeしています。
また、毎度毎度webpack関係のコマンドを打つのは面倒なのでビルドやテスト関係のコマンドは全てpackage.jsonのscriptに登録しておきました。

```javascript:package.json
  "scripts": {
    "dev-build": "webpack --config webpack.dev.js",
    "prod-build": "webpack --config webpack.prod.js",
    "test": "nyc ava --verbose",
    "test-transpile": "dev-build && yarn test"
  },
```

```javascript:webpack.dev.js
var nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: 'development',
  externals: [nodeExternals()],
  target: 'node',
  devtool: 'inline-source-map',
  entry: './src/main.js',
  output: {
    library: 'nomlish',
    libraryTarget: 'umd'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

```

```javascript:webpack.prod.js
var nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: 'production',
  externals: [nodeExternals()],
  target: 'node',
  entry: './src/main.js',
  optimization: {
    minimize: true,
  },
  output: {
    library: 'nomlish',
    libraryTarget: 'umd'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

```

# 感想
構想自体は既にあったのですがnpmライブラリを公開する作法的な所で結構詰まりました。また、babelとwebpackまわりを使ってトランスパイルする手順でかなり詰まりました。時間としてはほぼここらへんでめっちゃ使っていたのでやはり環境を作るまでが難しかったです。
僕はこのライブラリを使ってSlackの人の発言をノムリッシュ翻訳して投稿したりツイッターの発言をすべて投稿する前にノムリッシュ翻訳させたりする未来が来る事を願っています

# ノムリッシュ翻訳サイト
http://racing-lagoon.info/