elixir地域化の話その2

前回のExgettextは、ビルドが面倒だったり、mixのdepsへ入れると
うまく連動しないとか不満があったので、その辺を整備してみました。

不満1 mix depsに入れるとexmoがビルドされない

当たり前で、deps.compileではMix.Tasks.Compileが呼ばれます。Mix.Tasks.l10n.msgfmtなどという意味不明なタスクは呼ばれません。depsの設定でカスタムコンパイラを設定する事もできますが、それはmakeなどを想定しているもので、うまく連動しません。

そこで、Mix.Tasks.Compile.Poという新しいコンパイラタスクを作成しました。やっている事は、Mix.Task.runでl10n.msgfmtを呼び出すだけです。

mixはコンパイラをMix.compilersという設定で持っていて、Mix.Project.configで上書きできます。したがって、mix.exsに

defmodule L10nElixir.Mixfile do
  use Mix.Project

  def project do
    [app: :l10n_elixir,
     version: "0.0.1",
     elixir: "~> 0.15.1-dev",
     compilers: Mix.compilers ++ [:po],
     deps: deps]
  end
  def deps,  do: []
end

のようにしておけばよいです。

不満点2 Code.get_docs/2が面倒くさい

Code.get_docs/2は、モジュールからドキュメントを抽出する関数ですが、この時点で翻訳リソースを出力してほしいのですが、Codeモジュールに手を入れるのはチキンな私には出来ません。そこで、aliasを使い、ごまかしを企むことにしました。

use Exgettext
...
Code.get_docs(Code, :all)

とすると、CodeモジュールはExgettext.Codeモジュールを参照するという仕組みです。そのためには、Exgettext.Codeモジュールは、Codeモジュールの全ての公開関数を実装している必要があります。イメージは、

defmodule Exgettext.Code do
  Elixir.Code.__info__(:exports) |>
  Enum.filter(&(&1 !== {:get_docs, 2})) |>
  Enum.map(&(defdelegate &1, to: Elixir.Code))
  def get_docs do
    something
  end
end

という感じで、get_docs/2以外をぜーんぶdelegateしてしまおうという
企みです(上のコードは動作しません)。
これを、defdelegate_filter/3という形で実装しました。
使い方は、

defmodule Exgettext.Code do
  defdelegate_filter(Exgettext.Code, Code, &(not &1 in [{:get_docs, 2}]))
  def get_docs(module, kind) do
     ...
  end
end

という感じです(実際はモジュール修飾がついてExgettext.Util.defdelegate_filterとかかっこ悪くなります)。

defdelegate_filterは、大きく4つの仕事をします。

1. ターゲットの公開関数を取得
2. ユーザから渡されたフィルタの適用と、デフォルトで実装される関数(__info__, module_info)を除外
3. アリティから仮引数の作成
4. 関数名、仮引数のリストから、defdelegate funcname(arg1,..argn), to: target
を作成して、それを実行

defdelegate funcname(arg,..,argn), to: targetは、quoteすると分かりますが、

{{:., [], [Kernel, :defdelegate]}, [],
                      [{funcname, [], args}, [{:to, target}]]}

です。argsは、{:aN, [], nil}のリストです(Nは整数)。このタプルを作るのが前半です。
後半の実行は、Code.eval_quoted/2ではなく、Module.eval_quoted/2を使います。
それらを盛り込むと、こんな感じに実装できます。

  def defdelegate_filter(src, target, func) do
    target.module_info(:exports) |>
      Stream.filter(fn({ff, a}) ->
                      func.({ff, a}) && (not ff in [:__info__, :module_info])
                    end) |>
      Stream.map(fn({ff, a}) ->
                   args1 = :lists.seq(1,a)
                   {ff, Enum.map(args1, fn(x) -> {:"a#{x}", [], nil} end)}
                 end) |>
      Enum.map(fn({ff, a}) ->
                 # IO.inspect ff                                                     
                 r = {{:., [], [Kernel, :defdelegate]}, [],
                      [{ff, [], a}, [{:to, target}]]}
                 # IO.puts Macro.to_string(r)                                        
                 Module.eval_quoted(src,r)
	       end)
  end

ex_docへのパッチ


パッチはExgettext.Codeを使うためのalias一行ですみます。

diff --git a/lib/ex_doc/retriever.ex b/lib/ex_doc/retriever.ex
index 40b07d9..7f3ecde 100644
--- a/lib/ex_doc/retriever.ex
+++ b/lib/ex_doc/retriever.ex
@@ -1,3 +1,4 @@
+use Exgettext
 defmodule ExDoc.ModuleNode do
   defstruct id: nil, module: nil, moduledoc: nil,
     docs: [], typespecs: [], source: nil, type: nil
air13:ex_doc k-1$ 

実際は、ロードパスの追加等他の要素もありますが。。。

システムへのインストール

この日本語環境をデフォルトで使用したい場合、exgettext, l10n_iex, l10n_elixirのそれぞれのディレクトリでmix archive.buildを実行して、ezパッケージを作成し、/usr/local/lib/elixir/libなどへコピーしてからunzipで展開すると、デフォルトで使えるようになります。
~/.iex.exsに、以下を記述するとhコマンドで翻訳済みのドキュメントを参照できるので幸せになります。

import Exgettext.Helper

成果物は、GitHub - k1complete/exgettext: yet another gettext localization package for elixir
GitHub - k1complete/l10n_iex: localization of IEx
GitHub - k1complete/l10n_elixir: l10n for elixir
GitHub - k1complete/ex_doc: ExDoc produces HTML and online documentation for Elixir projectsあたりにおいてあります。