README.md

# Localize Translate

## Installation

The package can be installed by adding `localize_translate` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:localize_translate, "~> 0.1"}
  ]
end
```

Documentation can be found at [https://hexdocs.pm/localize_translate](https://hexdocs.pm/localize_translate).

## Attribution

`localize_translate` derives from [trans](https://github.com/crbelaus/trans) by [@crbelaus](https://github.com/crbelaus) and its CLDR-integrated fork [ex_cldr_trans](https://github.com/elixir-cldr/cldr_trans). It is the continuation of that work within the `localize` ecosystem, built on [`:localize`](https://hex.pm/packages/localize) for CLDR-aware locale validation and parent-chain fallback.

### Introduction

`localize_translate` provides a way to manage and query translations embedded into schemas and removes the necessity of maintaining extra tables only for translation storage.

## Optional Requirements

Having Ecto SQL and Postgrex in your application will allow you to use the `Localize.Translate.QueryBuilder` component to generate database queries based on translated data. The runtime `Localize.Translate.translate/2,3` functions work without those dependencies.

- [Ecto SQL](https://hex.pm/packages/ecto_sql) 3.0 or higher
- [PostgreSQL](https://hex.pm/packages/postgrex) 9.4 or higher (since `Localize.Translate` leverages the JSONB datatype)

## Why Localize Translate?

The traditional approach to content internationalization consists on using an additional table for each translatable schema. This table works only as a storage for the original schema translations. For example, we may have a `posts` and a `posts_translations` tables.

This approach has a few disadvantages:

- It complicates the database schema because it creates extra tables that are coupled to the "main" ones.
- It makes migrations and schemas more complicated, since we always have to keep the two tables in sync.
- It requires constant JOINs in order to filter or fetch records along with their translations.

The approach used by `Localize.Translate` is based on modern RDBMSs support for unstructured datatypes. Instead of storing the translations in a different table, each translatable schema has an extra column that contains all of its translations. This approach drastically reduces the number of required JOINs when filtering or fetching records.

`Localize.Translate` is lightweight and modularized. The `Localize.Translate` module provides the `use` macro for declaring translatable schemas, the runtime `translate/2,3` functions, and field reflection. `Localize.Translate.QueryBuilder` provides the `Ecto.Query` macros for filtering and selecting translated values in SQL.

## Quickstart

Imagine that we have an `Article` schema that we want to translate:

```elixir
defmodule MyApp.Article do
  use Ecto.Schema

  schema "articles" do
    field :title, :string
    field :body, :string
  end
end
```

### Add a JSON column

The first step would be to add a new JSON column to the table so we can store the translations in it.

```elixir
defmodule MyApp.Repo.Migrations.AddTranslationsToArticles do
  use Ecto.Migration

  def change do
    alter table(:articles) do
      add :translations, :map
    end
  end
end
```

### Generate database function migration

`localize_translate` defines a Postgres database function to support in-db field translation. A migration task is provided to generate the migration required to define this function.

```elixir
% MIX_ENV=test mix localize.translate.gen.translate_function
* creating priv/repo/migrations
* creating priv/repo/migrations/20220307212312_localize_translate_gen_translate_function.exs
```

### Run migrations

Migrate the database to add the translations column and define the database function.

```elixir
% mix ecto.migrate
```

### Add translations to schema

Once we have the new database column, update the Article schema to declare translatable fields and the configured locales:

```elixir
defmodule MyApp.Article do
  use Ecto.Schema
  use Localize.Translate,
    translates: [:title, :body],
    locales: [:en, :es, :fr],
    default_locale: :en

  schema "articles" do
    field :title, :string
    field :body, :string
    # use the 'translations' macro to set up a map-field with a set of nested
    # structs to handle translation values for each configured locale and each
    # translatable field
    translations :translations
  end
end
```

The `:default_locale` field stores its values in the main schema columns; only the other locales get embedded translation fields.

### Casting translations

`localize_translate` will generate a simple default changeset for the translations field. It looks like this:

```elixir
def changeset(fields, params) do
  fields
  |> cast(params, list_of_translatable_fields)
  |> validate_required(list_of_translatable_fields)
end
```

That may not be flexible enough for all requirements. A custom changeset can be defined for the translations field:

```elixir
  def changeset(article, params \\ %{}) do
    article
    |> cast(params, [:title, :body])
    |> cast_embed(:translations, with: &translations_changeset/2)
    |> validate_required([:title, :body])
  end

  defp translations_changeset(translations, params) do
    translations
    |> cast(params, [])
    |> cast_embed(:es)
    |> cast_embed(:fr)
  end
end
```

### Query Building

After the schema is configured, use `Localize.Translate.translate/2,3` to fetch translations and `Localize.Translate.QueryBuilder` to query them:

```elixir
# Translate a single field, with fallback chain
Localize.Translate.translate(article, :title, [:de, :es])

# Translate the whole struct (and its embeds/associations) into Spanish
Localize.Translate.translate(article, :es)

# Filter on a translation in a query
from a in Article,
  where: translated(Article, a.title, :fr) == "Elixir"
```

Locales are validated via `Localize.validate_locale/1`, so atoms (`:en`), strings (`"en"`), and `Localize.LanguageTag` structs are all accepted. Fallback chains follow CLDR parent locales automatically — for example, `Localize.Translate.translate(article, :title, Localize.LanguageTag.new!("en-AU"))` walks `:"en-AU"` → `:"en-001"` → `:en`.