仕事の状況でも

早速一日さぼってしまった。
例によって一文字目を書き出せないため。
晩御飯にこっちのお父さんに付き合って一杯やったのが誤算だったなぁ(言い訳)


今日は仕事の状況でも書いてみる。

今やってる仕事

今やってるのは経理ソフトの多言語化対応。
何回か書いたけど、リソースファイルという仕組みではなく、翻訳エンジンってのを使ってる。
システムが幸いJavaServletなので、フィルタを使ってリクエストとレスポンスを横取りし、それぞれ翻訳した形に直す。


Javaだとフィルタが使えるけど、PHPとかだと元のソースいじって独自フィルタ作るか、Webサーバ依存になるけど各種Webサーバのモジュール作ってやる必要あり。
とはいえ、アーキテクチャ自体はどんなシステムもリクエスト/レスポンス型のシステムである以上は使える。

状況

キャッシュを除いて一応終わった。キャッシュ部分は今週中か来週中くらいの予定。ざっと設計した感じ、今週中でいけそう。ただ翻訳チームの進捗の邪魔になるから、リリースは来週にするかもしれんが。
なおテストが不十分だけど、予想では一部後述する逆翻訳部分で調整しないといけないところが残っていると思われる。


金銭的にもこの仕事をいただけたおかげで大分安定したのでよかった。

構造

クラス図でもかこうと思ったけどVisioがねぇや。なんか貧弱な環境だなぁ。
仕方ないのでLibreOffice Drawで地道に書いてみた。200%くらい時間かかって30%くらいに品質が落ちてる気がするな。

Request

Response


Response時の基本翻訳処理

Response時の翻訳は、基本HTMLの解析なので、本当は上記図面にはもっといろんなオブジェクトがあるんだけど、ごちゃごちゃしすぎるので省略。
この4パターンの翻訳がある

  • 属性翻訳(例: <input type="text" value="こんにちは" />)
  • 要素内テキスト翻訳(例: <textarea>こんにちは</textarea>)
  • JavaScriptのテキスト翻訳(例: var greeting = "こんにちは";)
  • JavaScriptのダイアログScript翻訳(例: alert(greeting))


画像翻訳は都合によりやってない。都合というか、今回のシステムは画像がほとんどなかったんだけど。
汎用エンジンにするなら、翻訳するかどうかはともかく仕組みに対応しておいた方がいいと思うけどね。


HTML解析はjericho HTML Parser、JavaScript解析は独自。
jerichoは書き換えが得意なエンジンじゃないので、書き換えは頑張って自分でやること。まあ全部自分で作っちゃうのが一番早い気もする、と今は思うが。なんかHTML解析って、特殊文字の処理とか面倒くさいイメージがあってやる気になんなかったんだよな。


属性翻訳と要素内テキスト翻訳、JavaScriptのテキスト翻訳はまあわかるだろうから解説省略。
JavaScriptのダイアログScript翻訳ってのがちょっと特殊。
今回のケースではこういうのが結構あったんだよね。

alert(mode+"しますがよろしいですか?");

これをそのまま「しますがよろしいですか?」の部分だけを翻訳することはもちろん可能なんだけど、"Edit it? are you sure?"とかわけのわからんうざい翻訳になるので、ここはスクリプトごと翻訳にした。
普通は文字列翻訳で「しますがよろしいですか?」だけが翻訳対象なのを、「mode+"しますがよろしいですか?"」を対象にする。modeの場所を移せるように、場合によっては消せるように、って事すね。


で、こういうケースはアウト。

var greeting;
greeting = "こん";
greeting += "にちは";
alert(greeting);

「こん」と「にちは」が分かれて「こんにちは」を生成してる。
このレベルになっちゃうとJavaScript側で翻訳エンジン用意しないと駄目かなぁと思うけど、幸い今回のシステムには存在しないケースだったので省略。

Javaの面倒なところの処理

Java Servletのフィルタって基本的な横取り機能が無いから、RequestWrapperとResponseWrapperの自作が必要。
なぜかネットにもあまり使えるソースが落ちてないのでいろいろやって作る。
リクエストを書き換えるResponseWrapperの自作については、前俺自身もエントリ書いてたのでこの辺から。
http://d.hatena.ne.jp/tsugehara/20120820/1345452593


RequestWrapperはとりあえずこれで動く。ResponseWrapper側のソース例で載ってるフィルタのarg0ってなってるのを、このRequestWrapperのインスタンスに差し替えればOK。

import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class GenericRequestWrapper extends HttpServletRequestWrapper {
	Map<String, String[]> parameters;

	public GenericRequestWrapper(HttpServletRequest request) {
		this(request, null);
	}
	public GenericRequestWrapper(HttpServletRequest request, String charset) {
		super(request);
		if (charset != null)
			try {
				this.setCharacterEncoding(charset);
			} catch (UnsupportedEncodingException e) {
				throw new RuntimeException(e);
			}

		this.parameters = new HashMap<String, String[]>(request.getParameterMap());
	}

	@Override
	public String getParameter(String name) {
		String[] tmp = this.parameters.get(name);
		if (tmp == null || tmp.length == 0)
			return null;
		return tmp[0];
	}

	@Override
	public Map<String, String[]> getParameterMap() {
		return this.parameters;
	}

	@Override
	public Enumeration<String> getParameterNames() {
		return new EnumeratorWrapper<String>(
			this.parameters.keySet().iterator()
		);
	}

	@Override
	public String[] getParameterValues(String name) {
		for (String tmp : this.parameters.get(name))
			System.out.println(name+"="+tmp);
		return this.parameters.get(name);
	}

	public void setParameter(String name, String value) {
		this.parameters.put(name, new String[] {value});
	}
}

翻訳エンジンについて

翻訳エンジンはここで書いたロジック使ってる。
http://d.hatena.ne.jp/tsugehara/20120628/1340891158
特に翻訳量が十分でない場合、ものすごい量のクエリが飛ぶため遅い。


ということでキャッシュが必要。
キャッシュは単純に翻訳前レスポンス(のハッシュ)と翻訳先言語をPKで持つテーブルに、翻訳結果を格納しておけばOK。
適当なタイミングで破棄する機構だけ必要。

Requestの逆翻訳について

Responseの方は安定してるけど、当初気づいていなかった問題が一個あって、Requestの逆翻訳が必要だったんだよね。
特に今回の例ではinputを表形式に並べる画面が多いから、先に触れた4パターン翻訳の内属性翻訳を欠かすことができないんだけど、属性翻訳をしてしまうとリクエストの値が変わってしまうんだ。

<input type="text" value="こんにちは" />

が、

<input type="text" value="Hello" />

になるということなので、例えば「こんにちは」をキーにどこそこのテーブルから値を引っ張ってくる、なんて処理が壊れる。
または、こういう値があったとして、

<input type="text" value="現金出納簿" />

これを英語版で表示するとこうなるわけだけど、

<input type="text" value="Cash book" />

何も編集してないのに、そのまま送信すると現金出納簿の名前がCash bookになってしまって、日本語版で見てもCash bookのまま。当然日本語版から他言語への翻訳で、Cash bookを翻訳するリソースなどないので、ベトナム語版なども壊れる。


ということでリクエスト値については逆翻訳の必要あり。
ちょっと面倒だけど、本来は逆翻訳専用のテーブルを持たないといけない。
現在はやってないので、例えば「こんにちは」を「Hello」と翻訳していて、「今日は」も「Hello」と翻訳している場合、Helloを逆翻訳するとどっちになるか予想がつかないので、たぶん現状はどっかで不具合起きてる。まだ見つかってないけど。

ほぼ終わった現時点での感想

逆翻訳は始める前に気づくべきだったけど、残念ながら盲点になってしまったので、予想よりちょっと大変だった。
Javaのやつが(都合により省略)
結果は何度か書いてるけど、一発翻訳されるからいいと思うよ。


元々、翻訳なんてプログラマーの領分じゃないんだよね。
なのに現状、多言語対応しようと思ったらソースコードいじってコミットしないといけないなんてバカげてんだよ。
まあ基幹ソフトとかは仕方ないにしても、Webアプリケーションについてはエンジン別に用意して、翻訳したい人はWikiみたいなページから適当に翻訳すればいいんだ。


次回のテーマ指向SNSでたぶん実験も兼ねてやってみると思う。
JavaScriptが多くなりそうだから、今の仕組みとは別に作らないといけないだろうけど。


ただ一般リリースするレベルにするには、逆翻訳用の辞書構築用の処理が必要そう。
逆翻訳はちょいと頭痛いなぁ。どうしたらいいんだべ。
入力と出力で2回翻訳しないといけない、なんて面倒だしね。
入力を基に辞書構築した上で、細部を微調整ってのがベストだとは思うんだけど。
なんとか逆翻訳自体を不要に出来ないもんかと考えるけど、たぶん不可能だ。悩ましい。