Search documentation of phoenix_kit

# PhoenixKit.Modules.Languages.DialectMapper (phoenix_kit v1.7.87)

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:

- `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")
    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

base_to_dialect(base_code)

Converts base language code to default dialect.

default_dialects()

Gets the default dialects map.

dialects_for_base(base_code)

Gets all available dialect codes for a base language.

extract_base(locale)

Extracts base language code from full dialect code.

resolve_dialect(base_code, user \\ nil)

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

valid_base_code?(base_code)

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