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-US" (default) or user.custom_fields["preferred_locale"] ("en-GB")
↓
Set Gettext: "en-US" or "en-GB"
↓
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)
User Preferences
Authenticated users can override default mappings:
- User prefers British English: sets
custom_fields["preferred_locale"]= "en-GB" - Visits
/en/dashboard - System uses "en-GB" for translations
- URLs remain
/en/(not/en-GB/)
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 dialect with user preference (stored in custom_fields)
iex> user = %User{custom_fields: %{"preferred_locale" => "en-GB"}}
iex> DialectMapper.resolve_dialect("en", user)
"en-GB"
iex> DialectMapper.resolve_dialect("en", nil)
"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.
Resolves the full dialect code for a user visiting 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"
Resolves the full dialect code for a user visiting a base language URL.
Resolution priority:
- User's saved preference (if authenticated and preference matches base code)
- Default dialect mapping for that base language
Examples
iex> user = %User{custom_fields: %{"preferred_locale" => "en-GB"}}
iex> DialectMapper.resolve_dialect("en", user)
"en-GB"
iex> user = %User{custom_fields: %{"preferred_locale" => "es-MX"}}
iex> DialectMapper.resolve_dialect("en", user)
"en-US" # Preference doesn't match base, use default
iex> DialectMapper.resolve_dialect("en", nil)
"en-US"
iex> guest = %{some_field: "value"}
iex> DialectMapper.resolve_dialect("es", guest)
"es-ES"Security
User preference only applied if it matches the requested base code. This prevents users from forcing unintended locales via preference tampering.
Graceful Degradation
If user preference becomes invalid (dialect disabled, typo, etc.), system falls back to default mapping. No crashes or errors.
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