ギャルゲーwith i18n-js

##はじめに
ギャルゲーの主人公にi18n-jsを使って個性を出させます。

- 今回使用するライブラリ
    - redux
    - redux-saga
    - i18n-js
    - react-router-dom

redux、redux-sagaの設定などはすでにしている前提で話を進めていきます。
##口調わけをする
今回の本題です。ここ以外のパートは全部読み飛ばして大丈夫です。
口調わけをするために、タイトルにも書いてある`i18n-js`というライブラリを使用します。
このライブラリを使えば比較的簡単にアプリケーションを多言語に対応させることができます。

**決してギャルゲーを作るためのライブラリではありません。**

まずは口調ごとに文言を設定していきます。

```react:normal.ts
export default {
  expression: '通常',
  greeting: 'こんにちは。',
  words_end: 'だよ。',
  is_your_name: 'の名前はなに？'
};

```

```react:formal.ts
export default {
  expression: '敬語',
  greeting: 'どうも。',
  words_end: 'です。',
  is_your_name: 'の名前はなんですか？'
};

```

とりあえず通常の口調(ギャルゲー主人公によくある「〜だよ」「〜かな？」のような柔和な口調)と敬語を作成しました。

次に設定用のファイルを作成し、上記の口調をインポートしていきます。

```react:setting.ts
import normal from './normal';
import formal from './formal';
const I18n = require('i18n-js');

I18n.fallbacks = false;
I18n.translations = {normal, formal};

export default I18n;

```

プレイヤーにより選択された口調をreducerで受け取ります、sagaでセットします。

```react:reducers/expression.ts
import {SelectExpression, SELECT_EXPRESSION} from '../actions/selectExpression';
import setting from '../expression/setting';

type ExpressionAction = SelectExpression;

export interface ExpressionState {
	expression: any;
}

const initialState: ExpressionState = {
	expression: setting.currentLocale(),
};

const expressionReducer = (state = initialState, action: ExpressionAction) => {
	switch (action.type) {
		case SELECT_EXPRESSION:
			return Object.assign({}, state, {
				expression: setting.currentLocale()
			});
		default:
			return state;
	}
};

export default expressionReducer;

```
```react:sagas/expression.ts
import {put, takeEvery} from 'redux-saga/effects';
import setExpression, {SET_EXPRESSION} from '../actions/setExpression';
import {SELECT_EXPRESSION} from '../actions/selectExpression';
import setting from '../expression/setting';

function* handleSetLanguage(action: ReturnType<typeof setExpression>) {
  let expression = action.payload;

  setting.locale = expression;
	yield (localStorage.language = expression);
	yield put({type: SELECT_EXPRESSION});
}

function* expressionSaga() {
	yield takeEvery(SET_EXPRESSION, handleSetLanguage);
}

export default expressionSaga;

```
これで口調わけのロジックが完成しました。

##主人公の設定画面を作成する
先程作成したロジックを使って、主人公の設定をしていきます。
名前、一人称、二人称、口調が設定できればこの世にいる大体のキャラクターの喋り方を網羅することができるはずです。
この４点を設定できる画面を作成してみましょう。

```react:Setting.tsx
import * as React from 'react';
import { Link } from "react-router-dom";
import {connect} from 'react-redux';
import {Dispatch} from 'redux';
import {GlobalState} from '../reducers/index';
import setExpression from '../actions/setExpression';
import selectExpression from '../actions/selectExpression';
import selectPerson from '../actions/person';

interface StateProps {
  selectExpression: typeof selectExpression;
}

interface DispatchProps {
  setExpression: typeof setExpression;
  selectPerson: typeof selectPerson
}

type Props = StateProps & DispatchProps;

interface State {
  expression: string;
  name: string,
  first_person: string,
  second_person: string
}

interface IMessageInputEvent extends React.FormEvent<HTMLInputElement> {
	target: HTMLInputElement;
}

class Setting extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = {
      expression: 'normal',
      name: '',
      first_person: '',
      second_person: ''
    };

    this.nameChange = this.nameChange.bind(this)
    this.firstPersonChange = this.firstPersonChange.bind(this)
    this.secondPersonChange = this.secondPersonChange.bind(this)
  }
  public handleClick(expression: string) {
    this.props.setExpression(expression);
    this.props.selectPerson({
      name: this.state.name,
      first_person: this.state.first_person,
      second_person: this.state.second_person,
    });
  }

  public nameChange(e: IMessageInputEvent){
    this.setState({
      name: e.target.value
    })
  }
  public firstPersonChange(e: IMessageInputEvent){
    this.setState({
      first_person: e.target.value
    })
  }
  public secondPersonChange(e: IMessageInputEvent){
    this.setState({
      second_person: e.target.value
    })
  }

public render() {
return (
<>
	<h1>初期設定</h1>

        <p>名前：</p>
        <input type='text' value={this.state.name} onChange={this.nameChange}></input>
        <p>一人称：</p>
        <input type='text' value={this.state.first_person} onChange={this.firstPersonChange}></input>
        <p>二人称：</p>
        <input type='text' value={this.state.second_person} onChange={this.secondPersonChange}></input>

        <p>口調選択：</p>
        <select name="color1" onChange={ e => this.setState({expression: e.target.value}) }>
            <option value="normal">通常</option>
            <option value="formal">敬語</option>
        </select>
        <br/>
        <Link to="/game" onClick={this.handleClick.bind(this, this.state.expression)}>
          START
        </Link>
</>
		);
	}
}

const mapStateToProps = (state: GlobalState) => ({
  selectExpression: state.expression.expression,
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
	setExpression: (expression: string | null) => dispatch(setExpression(expression)),
	selectPerson: (person: {name: string, first_person: string, second_person: string}) => dispatch(selectPerson(person)),
});

export default connect(
	mapStateToProps,
	mapDispatchToProps
)(Setting);
```
作りました。
テキストボックス、セレクトボックスに入力されたvalueをsetStateしてdispatchしてるだけなのでここはそんなに目新しいところはないですね。
適当に読み飛ばしてください。

この設定画面で得た値をゲーム画面に反映させます。

```react:Game.tsx
import * as React from 'react';
import './Game.css';
import {connect} from 'react-redux';
import {GlobalState} from '../reducers/index';
import img from './image.png'
import setting from '../expression/setting'

interface Props {
  name: string,
  first_person: string,
  second_person: string
}

class Game extends React.Component<Props> {

	public render() {
		return (
			<div className="container">
				<h1>ゲーム画面</h1>
        <img src={img} alt="img" />
        <div className="name">{this.props.name}</div>
        <div className="text">{setting.t('greeting')}{this.props.first_person}の名前は{this.props.name}{setting.t('words_end')}{this.props.second_person}{setting.t('is_your_name')}</div>
			</div>
		);
	}
}

const mapStateToProps = (state: GlobalState) => ({
  name: state.person.person.name,
  first_person: state.person.person.first_person,
  second_person: state.person.person.second_person,
});

export default connect(
	mapStateToProps
)(Game);
```
i18n-jsで作成した口調を参照するには、以下のようにコードを書けば値を取得できます。

```react
import setting from '../expression/setting'
{setting.t('greeting')}
```
これで設定画面で得た主人公の情報をゲーム画面に反映させることができました。
<img width="510" alt="スクリーンショット 2019-09-02 17.54.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439548/61f259da-5d51-b036-9183-adb4cc67c3c6.png">
<img width="1170" alt="スクリーンショット 2019-09-02 18.02.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439548/797728a1-20ef-a30b-88ba-c3aafd3b057e.png">

ファイルさえ作れば口調は無限に増やすことができます。ためしに武士っぽい口調を足してみましょう。

```react:gozaru.ts
export default {
  expression: '武士口調',
  greeting: 'ご機嫌よう。',
  words_end: 'でござる。',
  is_your_name: '、名は何と申す？'
};
```
<img width="1158" alt="スクリーンショット 2019-09-02 18.08.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439548/f7c77d3a-4593-d9ae-3d39-f75eae41d941.png">
こころなしかヒロインの少女のことも御台所のように見えてきますね。気分は安土桃山です。


##おまけ
i18n-jsを使っている状態で定数ファイルから文言をimportするときはアロー関数を使わないといけないようです。

```react:constants.ts

import setting from '../expression/setting';

export const greeting = () => {
	return {
		'通常': setting.t('normal-greeting'),
		'チャラい': setting.t('chara-greeting'),
		'根暗': setting.t('nekura-greeting'),
	};
};
```
主人公の口調を変更した上で更にプレイヤーに主人公の性格もある程度選択させたい時などにご活用ください。
##使用させていただいた素材サイト
[きまぐれアフター背景素材置き場](https://k-after.at.webry.info/)
[フリー素材：メッセージウィンドウ素材 その29](https://kopacurve.blog.fc2.com/blog-entry-503.html)
[立ち絵素材　わたおきば](https://wataokiba.net/)