Localize.Translate.Store.SiblingResource (Localize Translate v0.2.0)

Copy Markdown View Source

A translation store keeping translations in a sibling table, one row per (subject, locale).

This is the traditional relational model — posts and posts_translations — which Localize.Translate.Store.Embedded exists to avoid. It is offered because the embedded model cannot do some things a translation workflow needs:

  • Per-translation state. A status, a reviewer, a timestamp, or any other column that belongs to one locale's translation rather than to the record.

  • Per-locale permissions. A translator who may edit fr but not de.

  • Translation history. Row-level auditing tools track a sibling row; they cannot track one locale inside a jsonb column.

  • Concurrent editing. Two translators working on different locales write different rows rather than contending on one column.

If none of those apply, use the embedded store. It is simpler, needs no join, and is the default.

The sibling schema

You define it — this store does not generate one. It needs a foreign key to the subject, a locale column, and one column per translatable field:

defmodule MyApp.ArticleTranslation do
  use Ecto.Schema

  schema "article_translations" do
    field(:locale, :string)
    field(:title, :string)
    field(:body, :string)
    belongs_to(:article, MyApp.Article)
  end
end

A unique index on (article_id, locale) is strongly advised; without one, nothing stops two rows claiming the same translation.

Declaring it

The subject needs a virtual container field for loaded translations to live in, and the store needs to know the repo, the sibling schema, and the foreign key:

defmodule MyApp.Article do
  use Ecto.Schema

  use Localize.Translate,
    translates: [:title, :body],
    locales: [:en, :es, :fr],
    default_locale: :en,
    store:
      {Localize.Translate.Store.SiblingResource,
       repo: MyApp.Repo,
       schema: MyApp.ArticleTranslation,
       foreign_key: :article_id}

  schema "articles" do
    field(:title, :string)
    field(:body, :string)
    field(:translations, :map, virtual: true)
  end
end

How resolution works

Localize.Translate.Store.load_translations/2 fetches every translation row for the subject in one query and reshapes them into the same %{locale => %{field => value}} map the embedded store uses, placing it in the virtual container. Localize.Translate calls it once at the start of translate/2,3, so resolving several fields across a CLDR fallback chain costs that single query rather than one per field per candidate locale.

Reads therefore delegate to the embedded store, which already knows how to answer from that shape.

Options

  • :repo — the Ecto.Repo to query. Required.

  • :schema — the sibling Ecto.Schema module. Required.

  • :foreign_key — the column on the sibling schema pointing at the subject. Required.

  • :locale_field — the column holding the locale. Defaults to :locale.

  • :primary_key — the field on the subject the foreign key points at. Defaults to :id.