View Source Get Started with AshTrans
Installation
Add the dependency to your mix.exs
file and include :ash_trans
in your .formatter.exs
:
# In mix.exs
{:ash_trans, "~> 0.1.0"}
# In .formatter.exs
import_deps: [..., :ash_trans]
If you are using Cldr, add AshTrans to your providers:
use Cldr,
providers: [AshTrans],
locales: ["it", "en"]
Adding to a resource
To add translations to a resource, add the extension to the resource:
use Ash.Resource,
extensions: [..., AshTrans.Resource]
translations do
# Set `public?` to true or add `:translations` to the action's accept list for public access
public? true
# Add the fields you want to translate
fields [:name, :description]
# Add the locales, except the default locale since it will be directly on the resource
locales [:it]
# If you are using Cldr
locales MyApp.Cldr.AshTrans.locale_names()
end
Example usage
First, we need to create a domain:
defmodule MyApp.Domain do
use Ash.Domain
resources do
resource MyApp.Post
end
end
Then let's create a resource for it:
defmodule MyApp.Post do
@moduledoc false
use Ash.Resource,
domain: MyApp.Domain,
data_layer: Ash.DataLayer.Ets,
extensions: [AshTrans.Resource]
attributes do
uuid_v7_primary_key :id
attribute :title, :string, public?: true
attribute :body, :string, public?: true
end
actions do
defaults [:read, :destroy, update: :*, create: :*]
end
translations do
public? true
fields [:title, :body]
locales [:it]
end
end
With the setup complete, let's explore how to manage translations. The extension will define two embedded resources, Translations and Translations.Fields that will look like this:
defmodule MyApp.Post.Translations do
use Ash.Resource, data_layer: :embedded
attributes do
attribute :it, MyApp.Post.Translations.Fields
end
end
defmodule MyApp.Post.Translations.Fields do
use Ash.Resource, data_layer: :embedded
attributes do
attribute :title, :string, public?: true
attribute :body, :string, public?: true
end
end
The extension adds these to the original resource as an attribute:
defmodule MyApp.Post do
...
attributes do
...
attribute :translations, MyApp.Post.Translations, public?: true
end
...
end
By doing so, we can leverage the Ash framework to do the validation, storage and casting of the translation data.
Now we can use our resource like any other and have translations added by passing a map composed of locale keys and as values another map having the fields we want translated.
post =
Ash.Changeset.for_create(MyApp.Post, :create, %{
title: "Title",
body: "Body",
# Like so
translations: %{
it: %{
title: "Titolo",
body: "Corpo"
}
}
})
|> Ash.create!()
To translate our struct, we can call translate/2
or to translate just a field and have it returned translate_field/3
post_it = AshTrans.translate(post, :it)
%{title: "Titolo", body: "Corpo"} = post_it
"Titolo" = AshTrans.translate_field(post, :title, :it)
Full example with Phoenix Liveview, Ash and Cldr
First we need to install and configure Cldr, then add to the Cldr module the AshTrans provider:
defmodule MyApp.Cldr do
use Cldr,
providers: [AshTrans],
locales: ["it", "en"]
end
This allows us to leverage Cldr for managing available locales and the current locale, rather than handling it manually.
Let's use the resource we have defined above, and replace in translations the locales with:
translations do
locales MyApp.Cldr.AshTrans.locale_names()
end
Now let's create a form to create/update the Post resource:
defmodule MyAppWeb.Post.FormComponent do
@moduledoc false
use MyAppWeb, :live_component
alias AshPhoenix.Form
alias MyApp.Cldr
alias MyApp.Post
@impl true
def render(assigns) do
~H"""
<div class="mt-12">
<.simple_form
for={@form}
id="post-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input
type="select"
label={gettext("Language")}
field={@form[:locale]}
options={locale_options()}
/>
<.input
class={hide_input?(@form[:locale]) && "hidden"}
label={gettext("Title")}
field={@form[:title]}
/>
<.inputs_for :let={translations} field={@form[:translations]}>
<.inputs_for
:let={field}
:for={locale <- Cldr.AshTrans.locale_names()}
field={translations[locale]}
>
<.input
class={hide_translation_input?(@form[:locale], locale) && "hidden"}
label={gettext("Title")}
field={field[:title]}
/>
</.inputs_for>
</.inputs_for>
<.input
class={hide_input?(@form[:locale]) && "hidden"}
type="textarea"
label={gettext("Body")}
field={@form[:body]}
/>
<.inputs_for :let={translations} field={@form[:translations]}>
<.inputs_for
:let={field}
:for={locale <- Cldr.AshTrans.locale_names()}
field={translations[locale]}
>
<.input
class={hide_translation_input?(@form[:locale], locale) && "hidden"}
type="textarea"
label={gettext("Body")}
field={field[:body]}
/>
</.inputs_for>
</.inputs_for>
</.simple_form>
</div>
"""
end
@impl true
def update(%{post: post} = assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_form(assigns.live_action, post)}
end
@impl true
def handle_event("validate", %{"post" => params}, socket) do
form = Form.validate(socket.assigns.form, params)
{:noreply, assign(socket, :form, form)}
end
def handle_event("save", %{"post" => params}, socket) do
save_post(socket, socket.assigns.live_action, params)
end
defp save_post(socket, :edit, params) do
case Form.submit(socket.assigns.form, params: params) do
{:ok, post} ->
notify_parent({:saved, post})
{:noreply,
socket
|> put_flash(:info, gettext("Post updated successfully"))
|> push_patch(to: socket.assigns.patch, replace: true)}
{:error, form} ->
{:noreply, assign(socket, :form, form)}
end
end
defp save_post(socket, :new, params) do
case Form.submit(socket.assigns.form, params: params) do
{:ok, post} ->
notify_parent({:saved, post})
{:noreply,
socket
|> put_flash(:info, gettext("Post created successfully"))
|> push_navigate(to: socket.assigns.patch.(post))}
{:error, form} ->
{:noreply, assign(socket, :form, form)}
end
end
defp assign_form(socket, :new, _) do
form =
Form.for_create(Post, :create,
as: "post",
forms: [auto?: true],
prepare_source: fn changeset ->
Ash.Changeset.set_argument(changeset, :locale, current_locale())
end
)
|> AshTrans.add_forms(Cldr.AshTrans.locale_names())
assign(socket, :form, to_form(form))
end
defp assign_form(socket, :edit, post) do
form =
Form.for_update(post, :update,
as: "post",
forms: [auto?: true],
prepare_source: fn changeset ->
Ash.Changeset.set_argument(changeset, :locale, current_locale())
end
)
|> AshTrans.add_forms(Cldr.AshTrans.locale_names())
assign(socket, :form, to_form(form))
end
defp hide_input?(field) do
field.value && to_string(field.value) != to_string(default_locale())
end
defp hide_translation_input?(field, locale) do
!field.value || to_string(field.value) != to_string(locale)
end
defp default_locale do
Cldr.default_locale().cldr_locale_name
end
defp current_locale do
Cldr.get_locale().cldr_locale_name
end
defp locale_options do
Enum.map(Cldr.known_locale_names(), &{Cldr.LocaleDisplay.display_name!(&1), &1})
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end
Here we have used the Phoenix .inputs_for
component to manage the nested embedded resource translations, then when creating the form we used the helper in AshTrans add_forms/1
to add the necessary forms.
We use CSS to hide translation inputs rather than conditional rendering to preserve input data when switching languages.
The form component can now be used to create or update posts, along with the translations.
Now we can make a LiveView to display a post:
defmodule MyAppWeb.PostLive.Show do
use MyAppWeb, :live_view
alias MyApp.Cldr
alias MyApp.Post
@impl true
def render(assigns) do
~H"""
<h1>
<%= @post.title %>
</h1>
<p>
<%= @post.body %>
</p>
"""
end
@impl true
def mount(%{"id" => id}, _session, socket) do
post =
Ash.get!(Post, id)
|> Cldr.AshTrans.translate(post)
{:ok, socket |> assign(:post, post)}
end
end
Cldr handles passing the current locale to AshTrans, which can be set using various strategies, such as Cldr.Plug.