{"id":28430986,"url":"https://github.com/elixir-cldr/cldr_trans","last_synced_at":"2025-07-04T17:31:33.065Z","repository":{"id":43196426,"uuid":"465517502","full_name":"elixir-cldr/cldr_trans","owner":"elixir-cldr","description":"Cldr-based fork of the most excellent \"trans\" library","archived":false,"fork":false,"pushed_at":"2024-08-11T02:15:06.000Z","size":187,"stargazers_count":10,"open_issues_count":2,"forks_count":3,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-06-05T14:39:43.673Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elixir-cldr.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-03-03T00:48:04.000Z","updated_at":"2024-08-28T10:51:24.000Z","dependencies_parsed_at":"2024-08-11T04:31:44.284Z","dependency_job_id":null,"html_url":"https://github.com/elixir-cldr/cldr_trans","commit_stats":{"total_commits":32,"total_committers":3,"mean_commits":"10.666666666666666","dds":0.40625,"last_synced_commit":"e4dfe1ef84dab85a146739f6d5b47a2c232dce81"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/elixir-cldr/cldr_trans","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-cldr%2Fcldr_trans","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-cldr%2Fcldr_trans/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-cldr%2Fcldr_trans/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-cldr%2Fcldr_trans/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elixir-cldr","download_url":"https://codeload.github.com/elixir-cldr/cldr_trans/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-cldr%2Fcldr_trans/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263588082,"owners_count":23484860,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-05T14:30:45.212Z","updated_at":"2025-07-04T17:31:33.059Z","avatar_url":"https://github.com/elixir-cldr.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cldr Trans\n![Build status](https://github.com/elixir-cldr/cldr_trans/actions/workflows/ci.yml/badge.svg)\n[![Hex.pm](https://img.shields.io/hexpm/v/ex_cldr_trans.svg)](https://hex.pm/packages/ex_cldr_trans)\n[![Hex.pm](https://img.shields.io/hexpm/dw/ex_cldr_trans.svg?)](https://hex.pm/packages/ex_cldr_trans)\n[![Hex.pm](https://img.shields.io/hexpm/dt/ex_cldr_trans.svg?)](https://hex.pm/packages/ex_cldr_trans)\n[![Hex.pm](https://img.shields.io/hexpm/l/ex_cldr_trans.svg)](https://hex.pm/packages/ex_cldr_trans)\n\n## Installation\n\nThe package can be installed by adding `ex_cldr_trans` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:ex_cldr_trans, \"~\u003e 1.0\"}\n  ]\nend\n```\nDocumentation can be found at [https://hexdocs.pm/ex_cldr_trans](https://hexdocs.pm/ex_cldr_trans).\n\n## Attribution\n\n`ex_cldr_trans` is an `ex_cldr`-integrated fork of the excellent [trans](https://github.com/crbelaus/trans) by [@crbelaus](https://github.com/crbelaus). The enhancements in this library have been been submitted as a [PR](https://github.com/crbelaus/trans/pull/74) to `trans`. If the PR is accepted in the future then this library will revert to depending on it rather than maintaining a fork.  Such a change will not affect the functionality or API in this library.\n\nThe documentation in `cldr_trans` is edited from the original `trans` documentation.\n\n### Introduction\n\n`ex_cldr_trans` provides a way to manage and query translations embedded into schemas\nand removes the necessity of maintaining extra tables only for translation storage. It is an addon library to [ex_cldr](https://hex.pm/packages/ex_cldr).\n\n`ex_cldr_trans` is published on [hex.pm](https://hex.pm/packages/ex_cldr_trans) and the documentation\nis also [available online](https://hexdocs.pm/ex_cldr_trans/).\n\n## Optional Requirements\n\nHaving Ecto SQL and Postgrex in your application will allow you to use the `Cldr.Trans.QueryBuilder`\ncomponent to generate database queries based on translated data.  You can still\nuse the `Cldr.Trans.Translator` component without those dependencies though.\n\n- [Ecto SQL](https://hex.pm/packages/ecto_sql) 3.0 or higher\n- [PostgreSQL](https://hex.pm/packages/postgrex) 9.4 or higher (since `Cldr.Trans` leverages the JSONB datatype)\n\n\n## Why CLDR Trans?\n\nThe traditional approach to content internationalization consists on using an\nadditional table for each translatable schema. This table works only as a storage\nfor the original schema translations. For example, we may have a `posts` and\na `posts_translations` tables.\n\nThis approach has a few disadvantages:\n\n- It complicates the database schema because it creates extra tables that are\n  coupled to the \"main\" ones.\n- It makes migrations and schemas more complicated, since we always have to keep\n  the two tables in sync.\n- It requires constant JOINs in order to filter or fetch records along with their\n  translations.\n\nThe approach used by `Cldr.Trans` is based on modern RDBMSs support for unstructured\ndatatypes.  Instead of storing the translations in a different table, each\ntranslatable schema has an extra column that contains all of its translations.\nThis approach drastically reduces the number of required JOINs when filtering or\nfetching records.\n\n`Cldr.Trans` is lightweight and modularized. The `Cldr.Trans` module provides metadata\nthat is used by the `Cldr.Trans.Translator` and `Cldr.Trans.QueryBuilder` modules, which\nimplement the main functionality of this library.\n\n## Quickstart\n\nImagine that we have an `Article` schema that we want to translate:\n\n```elixir\ndefmodule MyApp.Article do\n  use Ecto.Schema\n\n  schema \"articles\" do\n    field :title, :string\n    field :body, :string\n  end\nend\n```\n\n### Add a JSON column\n\nThe first step would be to add a new JSON column to the table so we can store the translations in it.\n\n```elixir\ndefmodule MyApp.Repo.Migrations.AddTranslationsToArticles do\n  use Ecto.Migration\n\n  def change do\n    alter table(:articles) do\n      add :translations, :map\n    end\n  end\nend\n```\n\n### Generate database function migration\n\n`ex_cldr_trans` 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.\n\n```elixir\n% MIX_ENV=test mix cldr.trans.gen.translate_function\n* creating priv/repo/migrations\n* creating priv/repo/migrations/20220307212312_trans_gen_translate_function.exs\n```\n\n### Run migrations\n\nMigrate the database to add the translations column and define the database function.\n```elixir\n% mix ecto.migrate\n```\n\n### Define a backend module\n\nThe next step is to define an [ex_cldr](https://hex.pm/packages/ex_cldr) [backend module](https://hexdocs.pm/ex_cldr/readme.html#backend-module-configuration) that defines the configured locales and other information for supporting localised applications.  For example:\n\n```elixir\ndefmodule MyApp.Cldr do\n  use Cldr,\n    locales: [\"en\", \"de\", \"ja\", \"en-AU\", \"th\", \"ar\", \"pl\", \"doi\", \"fr-CA\", \"nb\", \"no\"],\n    providers: [Cldr.Trans]\n\nend\n```\n\nNote that for existing backend modules the only required step is to add `Cldr.Trans` to the list of `:providers`.\n\n### Add translations to schema\n\nOnce we have the new database column and the backend module, we can update the Article schema to include the translations:\n\n```elixir\ndefmodule MyApp.Article do\n  use Ecto.Schema\n  use MyApp.Cldr.Trans, translates: [:title, :body]\n\n  schema \"articles\" do\n    field :title, :string\n    field :body, :string\n    # use the 'translations' macro to set up a map-field with a set of nested\n    # structs to handle translation values for each configured locale and each\n    # translatable field\n    translations :translations\n  end\n```\n\n### Casting translations\n\n`ex_cldr_trans` will generate a simple default changeset for the translations\nfield. It looks like this:\n\n```elixir\ndef changeset(fields, params) do\n  fields\n  |\u003e cast(params, list_of_translatable_fields)\n  |\u003e validate_required(list_of_translatable_fields)\nend\n```\n\nWhich means that it is only checking that translations are non-empty for each translatable\nfield in each locale configured in `MyApp.Cldr`.  That may not be flexible enough for all\nrequirements. Therefore a custom changeset may need to be defined for the translations field.\nHere's one example:\n\n```elixir\n  def changeset(article, params \\\\ %{}) do\n    article\n    |\u003e cast(params, [:title, :body])\n    # use 'cast_embed' to cast values for the 'translations' field.\n    |\u003e cast_embed(:translations, with: \u0026translations_changeset/2)\n    |\u003e validate_required([:title, :body])\n  end\n\n  # This is the changeset that will be invoked by the\n  # cast_embed/3 call above.\n  defp translations_changeset(translations, params) do\n    translations\n    |\u003e cast(params, [])\n    # use 'cast_embed' to handle values for translated fields for each of the\n    # configured languages with a changeset defined by the 'translations' macro\n    # above. Assumes that `:en` (the default), `:es` and `:fr` are configured in\n    # `MyApp.Cldr`.\n    |\u003e cast_embed(:es)\n    |\u003e cast_embed(:fr)\n  end\nend\n```\n\n### Query Building and Forms\n\nAfter doing this we can leverage the [Cldr.Trans.Translator](https://hexdocs.pm/ex_cldr_trans/Cldr.Trans.Translator.html) and [Cldr.Trans.QueryBuilder](https://hexdocs.pm/ex_cldr_trans/Cldr.Trans.QueryBuilder.html) modules to fetch and query translations from the database.\n\nThe translation storage can be managed using normal `Ecto.Changeset` functions just like any other field. Leveraging changesets and the [Phoenix.HTML.Form.inputs_for/2](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#module-nested-inputs) helper, your HTML form for `Article` might look like:\n\n```\n\u003c.form let={f} for={@changeset} phx-change=\"validate\" phx-submit=\"save\"\u003e\n  \u003c%= label f, :title %\u003e\n  \u003c%= text_input f, :title %\u003e\n  \u003c%= error_tag f, :title %\u003e\n\n  \u003c!-- translations for 'title' field --\u003e\n  \u003c%= inputs_for f, :translations, fn form_translations -\u003e %\u003e\n  \u003c%= for locale \u003c- [:es, :fr] do %\u003e\n    \u003c%= inputs_for form_translations, locale, fn form_locale -\u003e %\u003e\n    \u003cdiv\u003e\n      \u003cspan\u003e\u003c%= \"#{locale}\" %\u003e\u003c/span\u003e\n      \u003c%= text_input form_locale, :title, placeholder: Map.get(@changeset.data, :title) %\u003e\n    \u003c/div\u003e\n    \u003c%= error_tag form_locale, :title %\u003e\n  \u003c% end %\u003e\n\n  \u003c%= label f, :body %\u003e\n  \u003c%= text_input f, :body %\u003e\n  \u003c%= error_tag f, :body %\u003e\n\n  \u003c!-- translations for 'body' field --\u003e\n  \u003c%= inputs_for f, :translations, fn form_translations -\u003e %\u003e\n  \u003c%= for locale \u003c- [:es, :fr] do %\u003e\n    \u003c%= inputs_for form_translations, locale, fn form_locale -\u003e %\u003e\n    \u003cdiv\u003e\n      \u003cspan\u003e\u003c%= \"#{locale}\" %\u003e\u003c/span\u003e\n      \u003c%= text_input form_locale, :body, placeholder: Map.get(@changeset.data, :body) %\u003e\n    \u003c/div\u003e\n    \u003c%= error_tag form_locale, :body %\u003e\n  \u003c% end %\u003e\n\n  \u003c%= submit \"Save\" %\u003e\n\u003c/.form\u003e\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-cldr%2Fcldr_trans","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felixir-cldr%2Fcldr_trans","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-cldr%2Fcldr_trans/lists"}