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あたりにおいてあります。