Packages

phoenix_kit

1.7.160
1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules languages dialect_mapper.ex
Raw

lib/modules/languages/dialect_mapper.ex

defmodule PhoenixKit.Modules.Languages.DialectMapper do
@moduledoc """
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" → "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")
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"]
"""
alias PhoenixKit.Modules.Languages
# Default dialect mapping for most common variants
# Based on usage statistics and regional population
@default_dialects %{
"en" => "en-US",
# English
"es" => "es-ES",
# Spanish
"fr" => "fr-FR",
# French
"de" => "de-DE",
# German
"pt" => "pt-BR",
# Portuguese (Brazilian Portuguese more common)
"zh" => "zh-CN",
# Chinese (Simplified more common)
# Languages without regional variants map to themselves
"ar" => "ar",
# Arabic
"ja" => "ja",
# Japanese
"ko" => "ko",
# Korean
"it" => "it",
# Italian
"ru" => "ru",
# Russian
"hi" => "hi",
# Hindi
"bn" => "bn",
# Bengali
"pa" => "pa",
# Punjabi
"jv" => "jv",
# Javanese
"vi" => "vi",
# Vietnamese
"tr" => "tr",
# Turkish
"pl" => "pl",
# Polish
"uk" => "uk",
# Ukrainian
"th" => "th",
# Thai
"nl" => "nl",
# Dutch
"sv" => "sv",
# Swedish
"no" => "no",
# Norwegian
"da" => "da",
# Danish
"fi" => "fi",
# Finnish
"cs" => "cs",
# Czech
"hu" => "hu",
# Hungarian
"ro" => "ro",
# Romanian
"el" => "el",
# Greek
"he" => "he",
# Hebrew
"id" => "id",
# Indonesian
"ms" => "ms",
# Malay
"fa" => "fa",
# Persian
"sw" => "sw",
# Swahili
"ta" => "ta",
# Tamil
"te" => "te",
# Telugu
"mr" => "mr",
# Marathi
"ur" => "ur",
# Urdu
"gu" => "gu",
# Gujarati
"kn" => "kn",
# Kannada
"ml" => "ml"
# Malayalam
}
@doc """
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"
"""
# Default fallback for nil and empty strings
def extract_base(nil), do: "en"
def extract_base(""), do: "en"
def extract_base(locale) when is_binary(locale) do
locale
|> String.split("-")
|> List.first()
|> String.downcase()
end
@doc """
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"
"""
def base_to_dialect(base_code) when is_binary(base_code) do
base_lower = String.downcase(base_code)
Map.get(@default_dialects, base_lower, base_lower)
end
@doc """
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"
"""
def resolve_dialect(base_code) when is_binary(base_code) do
base_to_dialect(base_code)
end
@doc """
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
"""
def valid_base_code?(base_code) when is_binary(base_code) do
# Only validate if it looks like a base code (2 letters, no hyphen)
if String.length(base_code) == 2 and not String.contains?(base_code, "-") do
dialect = base_to_dialect(base_code)
Languages.get_predefined_language(dialect) != nil
else
false
end
end
@doc """
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)
"""
def dialects_for_base(base_code) when is_binary(base_code) do
base_lower = String.downcase(base_code)
Languages.get_available_languages()
|> Enum.filter(fn %{code: code} ->
extract_base(code) == base_lower
end)
|> Enum.map(& &1.code)
|> Enum.sort()
end
@doc """
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"
"""
def default_dialects, do: @default_dialects
@doc """
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([])
%{}
"""
def group_dialects_by_base(languages) when is_list(languages) do
languages
|> Enum.reduce([], fn lang, acc ->
case lang_code(lang) do
nil -> acc
code -> [extract_base(code) | acc]
end
end)
|> Enum.frequencies()
end
defp lang_code(%{code: code}) when is_binary(code), do: code
defp lang_code(%{"code" => code}) when is_binary(code), do: code
defp lang_code(_), do: nil
end