PhoenixKit.Modules.Languages.DialectMapper (phoenix_kit v1.7.71)

Copy Markdown View Source

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:

  1. 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
  2. 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:

  • enen-US (American English)
  • eses-ES (European Spanish)
  • ptpt-BR (Brazilian Portuguese)
  • zhzh-CN (Simplified Chinese)
  • dede-DE (German Germany)
  • frfr-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")
false

Getting 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

base_to_dialect(base_code)

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"

default_dialects()

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"

dialects_for_base(base_code)

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)

extract_base(locale)

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"

resolve_dialect(base_code, user \\ nil)

Resolves the full dialect code for a user visiting a base language URL.

Resolution priority:

  1. User's saved preference (if authenticated and preference matches base code)
  2. 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.

valid_base_code?(base_code)

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