Current section
Files
Jump to
Current section
Files
lib/gettext_mapper.ex
defmodule GettextMapper do
@moduledoc """
GettextMapper provides helper functions and macros for handling localized strings
with seamless integration into Gettext workflows.
This library allows you to work with translation maps that can be stored in databases
or other data structures, while maintaining compatibility with Gettext's extraction
and translation management tools.
## 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
- **Automatic Sync**: Keep your code in sync with .po file updates
- **Ecto Integration**: Built-in Ecto types for storing translations
## Basic Usage
defmodule MyApp.Product do
use GettextMapper
def name do
# Static translation map - extractable by Gettext
gettext_mapper(%{"en" => "Product Name"})
end
def description(translation_map) do
# Runtime localization of stored translations
GettextMapper.localize(translation_map, "No description")
end
end
## Domain Support
defmodule MyApp.AdminPanel do
use GettextMapper, domain: "admin"
def title do
gettext_mapper(%{"en" => "Admin Panel"})
end
def error_message do
# Override domain at call level
gettext_mapper(%{"en" => "Error occurred"}, domain: "errors")
end
end
## Database Integration
# In your schema
defmodule MyApp.Post do
use Ecto.Schema
schema "posts" do
field :title_translations, GettextMapper.Ecto.Type.Translated
field :content_translations, GettextMapper.Ecto.Type.Translated
end
def title(post) do
GettextMapper.localize(post.title_translations)
end
end
## Mix Tasks
- `mix gettext_mapper.sync` - Sync static maps with .po file changes
- `mix gettext_mapper.extract` - Extract translations to populate .po files
## Configuration
# config/config.exs
config :gettext_mapper,
gettext: MyApp.Gettext,
default_translation: "Missing Translation",
supported_locales: ["en", "de", "es", "fr"]
You can also specify a custom backend per module:
defmodule MyApp.SpecialModule do
use GettextMapper, backend: MyApp.SpecialGettext
def message do
gettext_mapper(%{"en" => "Special message"})
end
end
"""
alias GettextMapper.GettextAPI
@doc """
Imports GettextMapper macros into the current module.
## Options
- `:domain` - The Gettext domain to use for all `gettext_mapper/1` calls in this module.
Defaults to the configured default domain (usually "default").
- `:backend` - The Gettext backend module to use for this module.
Defaults to the globally configured backend.
## Examples
# Use default domain and backend
defmodule MyApp.User do
use GettextMapper
def greeting do
gettext_mapper(%{"en" => "Hello!"})
end
end
# Use specific domain
defmodule MyApp.AdminPanel do
use GettextMapper, domain: "admin"
def title do
gettext_mapper(%{"en" => "Admin Panel"})
end
end
# Use specific backend
defmodule MyApp.SpecialModule do
use GettextMapper, backend: MyApp.SpecialGettext
def message do
gettext_mapper(%{"en" => "Special Message"})
end
end
# Use both custom domain and backend
defmodule MyApp.CustomModule do
use GettextMapper, domain: "custom", backend: MyApp.CustomGettext
def content do
gettext_mapper(%{"en" => "Custom Content"})
end
end
"""
defmacro __using__(opts) do
domain = Keyword.get(opts, :domain, GettextMapper.GettextAPI.default_domain())
Module.put_attribute(__CALLER__.module, :__gettext_domain__, domain)
with {:ok, backend} <- Keyword.fetch(opts, :backend),
backend when is_atom(backend) and backend not in [nil, false, true] <-
Macro.expand(backend, __CALLER__) do
Module.put_attribute(__CALLER__.module, :__gettext_backend__, backend)
end
quote do
import GettextMapper.Macros
@gettext_mapper_domain unquote(domain)
# Set backend if provided, otherwise it will be nil and fallback to global config
# @__gettext_backend__ unquote(backend)
end
end
@doc """
Returns the localized string from a translations map for the current locale.
Falls back to the default locale, then to the given default value.
Examples, when the locale is "en" (default) and default fallback locale is "en":
# use current locale
iex> GettextMapper.localize(%{"en" => "Hello", "de" => "Hallo"}, "missed translation")
"Hello"
# use fallback locale
iex> GettextMapper.localize(%{"en" => "Hello", "nb" => "Hallo"})
"Hello"
iex> GettextMapper.localize(%{"da" => "Hallo", "fr" => "Bonjour"})
""
# use default translation
iex> GettextMapper.localize(%{"da" => "Hallo", "fr" => "Bonjour"}, "Default")
"Default"
"""
@spec localize(map() | nil, String.t()) :: String.t()
def localize(value, _default \\ "")
def localize(nil, _default), do: ""
def localize(value, default) when is_map(value) do
value[GettextAPI.locale()] || value[GettextAPI.default_locale()] || default
end
@doc """
Fetches the translation for the specified locale from a translations map.
Falls back to the default locale, then to the configured default translation
message if none is found.
The default message can be set via the `:default_translation` config
(defaults to "NO TRANSLATION").
"""
@spec translate(map(), String.t()) :: String.t()
def translate(values, locale) when is_map(values) do
cond do
str = Map.get(values, locale) -> str
str = Map.get(values, GettextAPI.default_locale()) -> str
true -> default_translation()
end
end
# Retrieves the default translation message from config, defaulting to "NO TRANSLATION".
defp default_translation do
Application.get_env(:gettext_mapper, :default_translation, "NO TRANSLATION")
end
end