Handles mapping between base language codes (en, es) and full dialect codes (en-US, es-MX).
This module provides the core logic for PhoenixKit's simplified URL architecture where URLs show base codes (/en/) but translations use full dialect codes (en-US).
Architecture
PhoenixKit uses a two-tier locale system:
Base Language Codes - Used in URLs for simplicity
- Format: 2-letter ISO 639-1 codes (en, es, fr, de, pt, zh, ja, etc.)
- Examples:
/en/dashboard,/es/admin,/fr/users - User-facing, SEO-friendly, easy to remember
Full Dialect Codes - Used internally for translations
- Format: BCP 47 language tags (en-US, es-MX, pt-BR, zh-CN)
- Examples: en-US, en-GB, es-ES, es-MX, pt-PT, pt-BR
- Translation-aware, respects regional differences
Data Flow
User visits: /en/dashboard
↓
Extract base: "en"
↓
Resolve dialect: "en" → "en-US" (default mapping for the base code)
↓
Set Gettext: "en-US"
↓
Generate URLs: Always use base code "en"Default Dialect Mapping
When no user preference exists, base codes map to most common regional variants:
en→en-US(American English)es→es-ES(European Spanish)pt→pt-BR(Brazilian Portuguese)zh→zh-CN(Simplified Chinese)de→de-DE(German Germany)fr→fr-FR(French France)
URL-Driven Resolution
Dialect resolution is purely URL-driven: a base code maps to its
default dialect and nothing else. A logged-in user's locale preference
is intentionally not consulted (see PhoenixKitWeb.Users.Auth for the
URL-is-authoritative rationale), so /en/dashboard always resolves to
the default en-US regardless of who is signed in.
Examples
# Extract base language from full dialect
iex> DialectMapper.extract_base("en-US")
"en"
iex> DialectMapper.extract_base("es-MX")
"es"
# Convert base to default dialect
iex> DialectMapper.base_to_dialect("en")
"en-US"
iex> DialectMapper.base_to_dialect("pt")
"pt-BR"
# Resolve a base code to its default dialect (URL-driven)
iex> DialectMapper.resolve_dialect("en")
"en-US"Validation
iex> DialectMapper.valid_base_code?("en")
true
iex> DialectMapper.valid_base_code?("xx")
falseGetting Available Dialects
iex> DialectMapper.dialects_for_base("en")
["en-US", "en-GB", "en-CA", "en-AU"]
iex> DialectMapper.dialects_for_base("es")
["es-ES", "es-MX", "es-AR", "es-CO"]
Summary
Functions
Converts base language code to default dialect.
Gets the default dialects map.
Gets all available dialect codes for a base language.
Extracts base language code from full dialect code.
Counts how many entries share each base language code in a list of language entries. Used by language switchers to decide whether to show a country qualifier or just the bare language name.
Resolves the full dialect code for a base language URL.
Validates if a base language code is supported.
Functions
Converts base language code to default dialect.
Uses predefined mapping for most common regional variants. Falls back to base code if no mapping exists.
Examples
iex> DialectMapper.base_to_dialect("en")
"en-US"
iex> DialectMapper.base_to_dialect("pt")
"pt-BR"
iex> DialectMapper.base_to_dialect("ja")
"ja"
iex> DialectMapper.base_to_dialect("xx")
"xx"
Gets the default dialects map.
Useful for debugging, testing, or documentation purposes.
Examples
iex> defaults = DialectMapper.default_dialects()
iex> defaults["en"]
"en-US"
iex> defaults["pt"]
"pt-BR"
Gets all available dialect codes for a base language.
Searches the predefined language list for all dialects matching the given base code.
Examples
iex> DialectMapper.dialects_for_base("en")
["en-US", "en-GB", "en-CA", "en-AU"]
iex> DialectMapper.dialects_for_base("es")
["es-ES", "es-MX", "es-AR", "es-CO"]
iex> DialectMapper.dialects_for_base("ja")
["ja"]
iex> DialectMapper.dialects_for_base("xx")
[]Use Cases
- Populate user preference dropdown
- Admin analytics (dialects per base language)
- Migration tools (find affected users)
Extracts base language code from full dialect code.
Splits on hyphen and returns first part (lowercased). Handles both dialect codes (en-US) and base codes (en). Returns "en" as default fallback for nil and empty string values.
Examples
iex> DialectMapper.extract_base("en-US")
"en"
iex> DialectMapper.extract_base("es-MX")
"es"
iex> DialectMapper.extract_base("zh-Hans-CN")
"zh"
iex> DialectMapper.extract_base("ja")
"ja"
iex> DialectMapper.extract_base("EN-GB")
"en"
iex> DialectMapper.extract_base(nil)
"en"
iex> DialectMapper.extract_base("")
"en"
Counts how many entries share each base language code in a list of language entries. Used by language switchers to decide whether to show a country qualifier or just the bare language name.
Accepts both maps with atom-keyed :code (e.g. %Language{} structs)
and string-keyed "code" (e.g. JSON-decoded settings). Entries
without a recognizable code are skipped.
Examples
iex> DialectMapper.group_dialects_by_base([
...> %{code: "en-US"},
...> %{code: "en-GB"},
...> %{code: "et-EE"}
...> ])
%{"en" => 2, "et" => 1}
iex> DialectMapper.group_dialects_by_base([])
%{}
Resolves the full dialect code for a base language URL.
The URL is authoritative: the base code maps straight to its default
dialect via base_to_dialect/1. User locale preferences are
deliberately NOT consulted here — locale resolution is URL-driven
across both the LiveView mount and the HTTP plug (see the rationale in
PhoenixKitWeb.Users.Auth). Resolving without a user keeps a logged-in
user's custom_fields["preferred_locale"] from silently upgrading e.g.
base "en" to "en-GB".
Examples
iex> DialectMapper.resolve_dialect("en")
"en-US"
iex> DialectMapper.resolve_dialect("es")
"es-ES"
iex> DialectMapper.resolve_dialect("ja")
"ja"
Validates if a base language code is supported.
Checks if the default dialect for this base code exists in the predefined language list.
Examples
iex> DialectMapper.valid_base_code?("en")
true
iex> DialectMapper.valid_base_code?("ja")
true
iex> DialectMapper.valid_base_code?("xx")
false
iex> DialectMapper.valid_base_code?("en-US")
false # Not a base code (contains hyphen)Notes
- Only validates base codes (2 letters)
- Full dialect codes will return false (use extract_base first)
- Checks against Languages.get_predefined_language/1