Common Lispで多言語対応のためgettextを使う

# Common Lispで多言語対応のためgettextを使う

## はじめに
普段Wordpressで、poとかmoとかを見かけるものの、ちゃんと向き合ったことがなかったのでCommon Lispの練習がてら、gettextを使ってみることにしました。

### 環境

- [Debian GNU/Linux](https://www.debian.org/) 10.2
- [ECL](https://common-lisp.net/project/ecl/) 16.1.3
- [gettext](https://www.gnu.org/software/gettext/) 0.19.8
- [cl-gettext](https://github.com/krzysz00/cl-gettext)
- [poedit](https://poedit.net/) 2.2.1

### 今回の内容のソースコード

https://gitlab.com/hu.moonstone/playgrounds/-/tree/master/locale-sample

## インストール

まずはaptで必要なツールをインストールしておきます。

```bash
$ sudo apt install gettext poedit
```

Poeditは、翻訳ファイルを作成するためのソフトウェアです。

## プロジェクト作成

https://gitlab.com/hu.moonstone/deepspace を使って、ひとまずプロジェクトの雛形を作成します。

```bash
$ deepspace -t project -o create -n locale-sample
$ cd locale-sample
$ tree
.
├── build
├── doc
├── lib
├── locale-sample-test.asd
├── locale-sample.asd
├── resources
│   ├── data
│   ├── images
│   └── sounds
├── src
│   ├── c
│   └── lisp
│       ├── locale-sample.lisp
│       └── package.lisp
└── test
    ├── c
    └── lisp
        ├── locale-sample-test.lisp
        └── package.lisp

14 directories, 8 files
```

ロケール用のファイルを保存するため、

```bash
$ mkdir -p resources/locale
```
で、ディレクトリを先に作成しておきます。

Quicklispなどでcl-gettextを導入し、ASDファイルを次のように記述します。

```common-lisp:locale-sample.asd
(asdf:defsystem :locale-sample
  :serial t
  :pathname "src/lisp"
  :components ((:file "package")
               (:file "locale-sample"))
  :depends-on (:asdf :alexandria :gettext))
```

その後、`src/lisp/package.lisp`に以下の内容を記述します。

```common-lisp:src/lisp/package.lisp
(in-package :cl-user)
(defpackage :locale-sample
  (:use :cl :gettext)
  (:export :main))
```

今回gettextを試すために書いたコードは以下のようなものになります。

```common-lisp:src/lisp/locale-sample.lisp
(in-package :locale-sample)

(setup-gettext :locale-sample "locale-sample")
(preload-catalogs #.(asdf:system-relative-pathname :locale-sample "resources/locale/"))

(defun replace-all (string part replacement &key (test #'char=))
    (with-output-to-string (out)
      (loop with part-length = (length part)
            for old-pos = 0 then (+ pos part-length)
            for pos = (search part string
                              :start2 old-pos
                              :test test)
            do (write-string string out
                             :start old-pos
                             :end (or pos (length string)))
            when pos do (write-string replacement out)
         while pos)))

(defparameter *labels* (make-hash-table))
(dolist (label `(,(_ "Dog")
                  ,(_ "Cat")
                  ,(_ "Penguin")))

  (let ((key (alexandria:make-keyword (string-upcase (replace-all label " " "-")))))
    (setf (gethash key *labels*) label)))

(defun main ()
  (setf *current-locale* "ja")
  (format t "-------------------------------~%")
  (format t "~A~%" (_ (gethash :dog *labels*)))
  (format t "~A~%" (_ (gethash :cat *labels*)))
  (format t "~A~%" (_ (gethash :penguin *labels*))))
```

## 初期化とリソースの位置を指定

`setup-gettext`により、gettextの初期化を行います。その上で、後ほど作成する翻訳ファイルの位置を`resources/locale/`以下にあるという形で指定します。

```common-lisp
(setup-gettext :locale-sample "locale-sample")
(preload-catalogs #.(asdf:system-relative-pathname :locale-sample "resources/locale/"))
```


## テキストのリストを用意

Poeditはソースコード中の特定のテキストを抽出することができるので、まずは翻訳対象のテキストの一覧を作成します。ここでは、`*labels*`というダイナミック変数にキーと値のペアで文字列を登録しておき、キーを呼び出すことで対応する文字列を取得できる形にしたいため、以下のような形で3つの単語をハッシュテーブルに登録します。

```common-lisp
(defparameter *labels* (make-hash-table))
(dolist (label `(,(_ "Dog")
                  ,(_ "Cat")
                  ,(_ "Penguin")))

  (let ((key (alexandria:make-keyword (string-upcase (replace-all label " " "-")))))
    (setf (gethash key *labels*) label)))
```

キーの名前は自動で作るようにしています。文字列情報を全て大文字に変更し、スペースがある場合はハイフンで繋ぐようにしました。これで、`Dog`は`:dog`でアクセスでき、`Iron Man`は、`:iron-man`でアクセスできるようになります。

このようにしてPoeditのソースから抽出する機能を使うと、3つの単語が一覧に表示されるため、対応する日本語のテキストを入力し保存します。mo、poファイルは`/resources/locale/ja/LC_MESSAGES`以下に、`locale-sample.mo`、`locale-sample.po`という名前で保存します。

![スクリーンショット_2020-02-16_11-32-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/568876/91e4c8f4-7c1f-2398-a249-933aca40511e.png)


## ロケールの設定

以下の箇所で、ロケールを日本語に変更しています。

```common-lisp
(setf *current-locale* "ja")
```


## 実行

```
;;; End of Pass 1.-------------------------------
犬
猫
ペンギン
```