GettextMapper
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"}
]
endThen fetch dependencies:
mix deps.getConfiguration
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
endUsage
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
end2. 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
end3. 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
endKey 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
endCustom Backend
Use a specific Gettext backend:
defmodule MyApp.Legacy do
use GettextMapper, backend: MyApp.LegacyGettext
endMix 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
- Extract messages:
mix gettext.extract(extracts fromgettext_mappercalls) - Generate templates:
mix gettext.merge priv/gettext - Translate .po files: Send to translators or translate manually
- Update code:
mix gettext_mapper.sync(updates static maps) - 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
fiAdvanced 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.ProductTranslationValidation
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
endTesting
Run the test suite:
# Run all tests
mix test
# With coverage
mix test --cover
# Run specific test files
mix test test/gettext_mapper_test.exsDocumentation
Full documentation is available at https://hexdocs.pm/gettext_mapper.
Contributing
We welcome contributions! Please:
- Fork the repository
- Create a feature branch
- Add tests for your changes
-
Ensure all tests pass:
mix test -
Run the formatter:
mix format - 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.