No description
  • Elixir 99.7%
  • Euphoria 0.3%
Find a file
2026-02-12 09:03:28 +01:00
lib feat: preserve original locale order when syncing translations 2026-02-02 23:00:44 +01:00
test fix: preserve original indentation when syncing multiline calls 2026-02-02 22:54:55 +01:00
.credo.exs init project 2025-08-26 22:44:44 +02:00
.formatter.exs docs: update README with badges and detailed usage examples 2025-06-25 22:41:17 +02:00
.gitignore init project 2025-08-26 22:44:44 +02:00
.tool-versions docs: update README with badges and detailed usage examples 2025-06-25 22:41:17 +02:00
CHANGELOG.md add CHANGELOG.md 2026-02-02 23:07:12 +01:00
LICENSE.md add License file 2026-02-12 09:03:28 +01:00
mix.exs bump version to 0.1.3 2026-02-02 23:03:16 +01:00
mix.lock support new version for gettext lib 2026-01-27 15:33:40 +01:00
README.md update Readme 2026-01-27 15:44:38 +01:00
TODO.md init project 2025-08-26 22:44:44 +02:00

GettextMapper

Hex.pm Coverage Status Docs

GettextMapper seamlessly bridges the gap between database-stored translations and Gettext's powerful internationalization tools. Store your translations as JSON maps in the database while maintaining full compatibility with Gettext's extraction, synchronization, and management workflows.

Features

  • 🗄️ Database Integration: Store translations as JSON maps in your database
  • 🔄 Gettext Compatibility: Extract messages for translation using standard Gettext tools
  • 🏷️ Domain Support: Use Gettext domains for organizing translations by context
  • 📦 Ecto Integration: Built-in Ecto types for seamless database operations
  • 🔄 Auto Synchronization: Keep your code in sync with .po file updates
  • 📝 Mix Tasks: Powerful CLI tools for managing translations
  • 🎯 Smart Fallbacks: Automatic fallback to default locale and custom defaults
  • Runtime Flexibility: Switch locales dynamically at runtime

Installation

Add gettext_mapper to your list of dependencies in mix.exs:

def deps do
  [
    {:gettext_mapper, "~> 0.1"}
  ]
end

Then fetch dependencies:

mix deps.get

Configuration

First, configure your Gettext backend in config/config.exs:

config :gettext_mapper,
  gettext: MyApp.Gettext,
  # Optional: custom message when no translation is found
  default_translation: "Missing Translation",
  # Optional: explicitly define supported locales (takes priority over auto-discovery)
  supported_locales: ["en", "de", "es", "fr"]

The supported_locales configuration is particularly useful when:

  • You want to restrict which locales are accepted in translation maps
  • Your application doesn't have .po files yet but you want to define supported locales
  • You want explicit control over locale validation instead of auto-discovery

Make sure you have a Gettext backend module:

defmodule MyApp.Gettext do
  use Gettext.Backend, otp_app: :my_app
end

Usage

Basic Example: Subscription Plans with Translations

Let's say you have subscription plans that need translated names and descriptions stored in the database.

1. Define your schema with translated fields:

defmodule MyApp.SubscriptionPlan do
  use Ecto.Schema

  schema "subscription_plans" do
    field :key, :string
    field :price, :decimal
    field :name, GettextMapper.Ecto.Type.Translated
    field :description, GettextMapper.Ecto.Type.Translated
    timestamps()
  end
end

2. Populate the database with translations using gettext_mapper:

defmodule MyApp.Seeds.Plans do
  use GettextMapper

  def seed do
    Repo.insert!(%SubscriptionPlan{
      key: "basic",
      price: Decimal.new("9.99"),
      name: gettext_mapper(%{
        "en" => "Basic Plan",
        "de" => "Basis-Tarif",
        "es" => "Plan Básico"
      }, msgid: "plan.basic.name"),
      description: gettext_mapper(%{
        "en" => "Perfect for individuals getting started",
        "de" => "Perfekt für Einsteiger",
        "es" => "Perfecto para comenzar"
      }, msgid: "plan.basic.description")
    })
  end
end

3. Display localized content based on user's locale:

# In your controller or view
plan = Repo.get_by!(SubscriptionPlan, key: "basic")

# Returns translation for current locale (e.g., "de")
GettextMapper.localize(plan.name)
#=> "Basis-Tarif"

GettextMapper.localize(plan.description, "No description available")
#=> "Perfekt für Einsteiger"

4. Or use lgettext_mapper for inline localized strings:

defmodule MyAppWeb.PlanController do
  use GettextMapper

  def index(conn, _params) do
    # Returns the localized string directly for current locale
    page_title = lgettext_mapper(%{
      "en" => "Choose Your Plan",
      "de" => "Wählen Sie Ihren Tarif"
    }, msgid: "plans.page_title")

    render(conn, :index, title: page_title)
  end
end

Key Concepts

Macro Returns Use Case
gettext_mapper/2 Map of all translations Storing in database
lgettext_mapper/2 Localized string Displaying to user
GettextMapper.localize/2 Localized string Reading from database

Additional Features

Custom Message IDs

Use stable translation keys instead of text as the gettext msgid:

gettext_mapper(%{"en" => "Hello", "de" => "Hallo"}, msgid: "greeting.hello")

This creates .po entries with stable keys that don't change when text changes:

msgid "greeting.hello"
msgstr "Hallo"

Domain Support

Organize translations by domain:

defmodule MyApp.Admin do
  use GettextMapper, domain: "admin"

  def title do
    lgettext_mapper(%{"en" => "Dashboard", "de" => "Übersicht"})
  end
end

Custom Backend

Use a specific Gettext backend:

defmodule MyApp.Legacy do
  use GettextMapper, backend: MyApp.LegacyGettext
end

Mix Tasks

GettextMapper provides powerful Mix tasks for managing your translations:

Extract Translations

Extract both message IDs and translations from your static gettext_mapper calls to populate .po files:

# Extract translations from all files
mix gettext_mapper.extract

# Extract from specific files  
mix gettext_mapper.extract lib/my_app/products.ex

# Dry run to see what would be extracted
mix gettext_mapper.extract --dry-run

# Use custom priv directory
mix gettext_mapper.extract --priv priv/my_gettext

This creates properly formatted .po files with both msgid and msgstr entries:

# In priv/gettext/de/LC_MESSAGES/default.po
msgid "Product Catalog"
msgstr "Produktkatalog"

# In priv/gettext/de/LC_MESSAGES/admin.po (domain-specific)
msgid "Admin Dashboard" 
msgstr "Verwaltungsdashboard"

Sync Translations

Keep your static translation maps in sync with .po file updates:

# Sync all files in lib/
mix gettext_mapper.sync

# Sync specific files
mix gettext_mapper.sync lib/my_app/controllers/

# Dry run to see what would change
mix gettext_mapper.sync --dry-run

# Generate translation map for a specific message
mix gettext_mapper.sync --message "Hello World"

When your .po files are updated (e.g., by translators), this task automatically updates your code:

# Before sync (outdated)
gettext_mapper(%{"en" => "Hello", "de" => "Hallo"})

# After sync (updated from .po files)  
gettext_mapper(%{"en" => "Hello", "de" => "Hallo Welt"})

Workflow Integration

Standard Gettext Workflow

  1. Extract messages: mix gettext.extract (extracts from gettext_mapper calls)
  2. Generate templates: mix gettext.merge priv/gettext
  3. Translate .po files: Send to translators or translate manually
  4. Update code: mix gettext_mapper.sync (updates static maps)
  5. Extract full translations: mix gettext_mapper.extract (populates .po files)

CI/CD Integration

# .github/workflows/translations.yml
- name: Check translation sync
  run: |
    mix gettext_mapper.sync --dry-run
    if [ $? -ne 0 ]; then
      echo "Translations are out of sync. Run 'mix gettext_mapper.sync'"
      exit 1
    fi

Advanced Features

Custom Ecto Types

Create domain-specific Ecto types:

defmodule MyApp.Types.ProductTranslation do
  use GettextMapper.Ecto.Type.Base, domain: "products"
end

# Use in schemas
field :name, MyApp.Types.ProductTranslation

Validation

Validate translation completeness:

def changeset(struct, attrs) do
  struct
  |> cast(attrs, [:name_translations])
  |> validate_translation_completeness(:name_translations)
end

defp validate_translation_completeness(changeset, field) do
  case get_field(changeset, field) do
    %{} = translations ->
      required_locales = GettextMapper.GettextAPI.known_locales()
      missing = required_locales -- Map.keys(translations)
      
      if Enum.empty?(missing) do
        changeset
      else
        add_error(changeset, field, "missing translations for: #{Enum.join(missing, ", ")}")
      end
    _ -> changeset
  end
end

Testing

Run the test suite:

# Run all tests
mix test

# With coverage
mix test --cover

# Run specific test files
mix test test/gettext_mapper_test.exs

Documentation

Full documentation is available at https://hexdocs.pm/gettext_mapper.

Contributing

We welcome contributions! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for your changes
  4. Ensure all tests pass: mix test
  5. Run the formatter: mix format
  6. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Changelog

See CHANGELOG.md for a detailed history of changes.