defmodule Mix.Tasks.Idiom.Extract do @moduledoc """ Extracts keys into a template directory that can be used for localisation. ## Setup In order to be able to extract keys, this task will hook into the compilation step. Since only macro calls are expanded at compile time, calls to `Idiom.t/3` directly will not work. To work around this, Idiom includes a `__using__/1` macro that exposes the same API while at the same time making calls available at compile time. If you are just getting started with Idiom, setting this up is easy: create a new module, for example `Project.Localisation` and `use Idiom` in it: ```elixir defmodule Project.Localisation do use Idiom end ``` Then, you can just `import Project.Localisation` and use `t/3` as in the documentation for Idiom. If you are currently directly calling `t/3` without `use`-ing Idiom, create the same module and in addition replace your `import Idiom` calls with `import Project.Localisation`. Afterwards, your keys will be available to `mix idiom.extract`. ## Arguments - `--base-locale` (required): Sets the base locale of the template. - `--data-dir`: Directory to extract template to. Will default to the `data_dir` configuration setting, or `priv/idiom` if neither command-line option nor setting exist. - `--default-namespace`: Default namespace. When the `idiom.extract` tasks finds a call to `Idiom.t/3` that passes `namespace` explicitly, it will be used for extractions. For situations where the namespace is not set explicitly but through `Idiom.put_namespace/1`, you can set the namespace to extract to using this option. - `--files`: Filters files to extract. When using the `default-namespace`, you will probably only want to extract keys from specific modules which can be selected using this option. Accepts everything that `Path.wildcard/1` accepts. ## Examples ```bash # Extract all keys with English as base mix idiom.extract --base-locale en # Extract all keys in the "signup" folder and add them to the "singup" namespace mix idiom.extract --base-locale en --default-namespace signup --files lib/project_web/live/signup/** ``` """ use Mix.Task @switches [ base_locale: :string, data_dir: :string, default_namespace: :string, files: :string ] @impl Mix.Task def run(args) do {opts, _} = OptionParser.parse!(args, switches: @switches) base_locale = Keyword.get(opts, :base_locale) || raise ArgumentError, """ Base locale has not been set. Please pass the `--base-locale` option. """ Idiom.Extract.start_link() Application.put_env(:idiom, :extracting?, true) Mix.Task.clear() Mix.Task.run("compile", ["--force"]) base_dir = Keyword.get(opts, :data_dir) || Application.get_env(:idiom, :data_dir) || "priv/idiom" template_dir = Path.join(base_dir, "template_#{base_locale}") File.mkdir_p!(template_dir) default_namespace = Keyword.get(opts, :default_namespace) || Idiom.get_namespace() || raise ArgumentError, """ No default namespace set. Please pass the `---default-namespace` locale or add a default namespace to your `config.exs`. """ Idiom.Extract.keys() |> Enum.filter(fn %{file: file} -> included?(file, opts) end) |> Enum.map(fn %{has_count?: true, plural_type: plural_type} = data -> generate_suffix_keys(data, base_locale, plural_type) key -> key end) |> List.flatten() |> Enum.group_by(fn %{namespace: nil} -> default_namespace %{namespace: namespace} -> namespace end) |> Enum.map(fn {namespace, data} -> data = data |> Enum.reduce(%{}, fn %{key: key}, acc -> Map.put(acc, key, key) end) |> Jason.encode!(pretty: true) template_dir |> Path.join([namespace, ".json"]) |> File.write!(data) end) end defp included?(file, opts) do opts |> Keyword.get(:files, "lib/**") |> Path.wildcard() |> Enum.map(&Path.expand/1) |> Enum.member?(file) end defp generate_suffix_keys(data, locale, plural_type) do locale |> Idiom.Plural.get_suffixes(plural_type) |> Enum.map(fn suffix -> Map.update!(data, :key, &(&1 <> "_" <> suffix)) end) end end