URL-driven locale resolution + admin-URL prefixless + module_assigns generalization#551
Merged
Conversation
The publishing dispatch override prepends its internal-prefix + discriminator segments to `conn.path_info` before letting Phoenix re-match. When the workspace mounts at a non-root path (the default `/phoenix_kit`), the prepended segments landed at the head of path_info while the registered internal routes are scoped UNDER the workspace prefix — so the rewritten path didn't match anything and every public publishing URL returned 404. Pass `url_prefix` through to `maybe_rewrite/2` so it can strip the prefix from path_info before the group-candidate check (otherwise `phoenix_kit` was mistaken for a language code) and re-prepend it to the rewritten path so it matches the registered routes.
`<.app_layout>` is a function component, so it only sees explicitly- declared attrs — conn assigns that aren't passed in are invisible to both the wrapper and the host's `Layouts.app` it forwards to. Publishing exposes a per-translation URL list as the conn assign `:phoenix_kit_publishing_translations` so host apps can build their own language switcher in their root or app layout with correct per-language URLs (important for posts with language-specific slugs that simple locale-prefix-rewrite gets wrong). The conn assign reached `root.html.heex` (which has full conn.assigns access) but NOT the inner `Layouts.app` function component called via `apply(module, function, [assigns])` — the assigns map there only contains what LayoutWrapper declared. Add an explicit `attr :phoenix_kit_publishing_translations` so when publishing's templates pass it through (separate commit on the publishing side), it lands in the assigns forwarded to the host's `Layouts.app`.
Replaces the publishing-specific `:phoenix_kit_publishing_translations` attr on `app_layout/1` with a generic `:module_assigns` map attr. Each key in the map is merged into the top-level assigns before the host's `Layouts.app` renders, so a host's custom layout can read e.g. `assigns[:phoenix_kit_publishing_translations]` from publishing — or any future module's host-consumable key — without core having to declare each one explicitly. Phoenix function-component attrs must be declared in advance and function-component layouts see ONLY declared attrs (not surrounding `conn.assigns`). The single map attribute lets each external module thread its own host-consumable keys through the boundary without core touching the API every time a new module ships a host integration. Merge policy: existing top-level assigns win over module-supplied keys, so a module can't accidentally clobber `:flash`, `:current_user`, or any other core-managed assign by colliding on the name. Cross-repo: phoenix_kit_publishing's html.ex render branches switch in the matching commit. The boundary test in `phoenix_kit_publishing/test/.../language_switcher_exposure_test.exs` continues to pin the chain end-to-end.
Mirrors the non-admin behaviour `Routes.path/2` already has: the primary language renders prefixless (`/phoenix_kit/admin/users`); every other locale keeps its segment (`/phoenix_kit/de/admin/users`). Both URL shapes resolve at the router level — the admin route macros declare `/:locale/admin/*` AND `/admin/*` scopes, so emitting prefixless for primary is safe and there's no redirect between the two shapes (typing `/phoenix_kit/en/admin/users` still works). Two helpers needed the same fix: the public `Routes.admin_path/2` API (used by `<.language_switcher_dropdown>`'s admin-path branch and by host code) and the private `build_admin_path/3` that handles `path/2`'s internal admin dispatch. Known tradeoff: switching locales inside admin via the UI today still crosses the `:phoenix_kit_admin_locale` ↔ `:phoenix_kit_admin` live_session boundary and forces a full-page reload. That's a separate piece of work (TODO 1 in `dev_docs/primary_language_no_prefix_plan.md`) covering router-macro restructure + a LiveView locale-rehydration hook + language-switcher push_patch refactor. Plan + Gemini/Codex review notes: dev_docs/primary_language_no_prefix_plan.md.
Pure URL→default locale resolution. The previous fallback chain
(URL → session → default) became a sticky-locale trap after
`Routes.path/2` was changed to emit prefixless URLs for the primary
language: visiting `/foo/et/...` once stashed `"et"` in the session,
then every primary-prefixless URL inherited Estonian forever — even
after the user explicitly switched back. Refreshing didn't fix it
because the session still held `et`.
Changes:
- `mount_phoenix_kit_current_scope/3` (line ~781) — drop the
`session_locale ||` step and the `Process.get(...)` fallback;
resolution is now URL `:locale` param → default.
- `maybe_update_locale_from_params/2` (the "no locale in params"
branch, line ~735) — drop the reserved-paths special case that
preserved the session locale on prefixless admin/api paths. All
prefixless paths now snap Gettext + assigns to the default locale,
matching what `Routes.path/2` emits.
- `renew_session/1` (line ~138) — drop the locale-preservation
dance across session renewal; nothing reads the key anymore.
- HTTP plug paths in `process_valid_locale/2` (line ~1869) and the
user-preference-resolution branch (line ~1763) no longer write
`:phoenix_kit_locale_base` to the session; it had no remaining
consumers.
Logged-in users still get their persisted preference via
`user.custom_fields["preferred_locale"]`, which is independent of
session and untouched by this change.
Verified in Chrome:
- `/phoenix_kit/et/users/log-in` renders "Logi sisse" (Estonian)
- `/phoenix_kit/users/log-in` renders "Log in" / "Sign in" (English)
- No need to clear cookies; refresh stays on whatever locale the
URL implies.
Three additional helper-level assertions covering the prefix-strip
edge cases Codex flagged in review:
* Explicit primary locale → prefixless (intended emission).
* Explicit non-primary locale → prefixed (the same shape any
legacy `/phoenix_kit/en/admin/...` URL takes — the dual-scope
route emission keeps both shapes resolvable in the router).
* Dialect-shaped code (e.g. "en-US") passes through unchanged.
Callers normalise to base first via DialectMapper, but pinning
this so a future "smart-strip" refactor doesn't accidentally
treat "en-US" like "en" and drop the prefix.
The full router-level backcompat assertion (live HTTP probe of
`/phoenix_kit/en/admin/...` returning 200) would need an integration
test endpoint; deferred. Helper-level pins are the cheap insurance.
Closes the plug-vs-LV asymmetry Codex flagged: the HTTP plug used to consult `user.custom_fields["preferred_locale"]` when the URL had no `:locale` segment, while the LV mount path (changed in `fecf3d3e`) went straight to default. A logged-in user with preferred_locale=et visiting `/phoenix_kit/admin/foo` would see Estonian for the initial HTTP response, then English after the LV mount snapped to default — visible content flash. Pure URL → default now holds across both paths. The preference field is still WRITTEN by the language-switcher click handler (`save_user_locale_preference/2`) so the data is preserved for any future feature that wants to re-enable preference reading; it just isn't read for routing today. Also removes the now-dead `get_user_preferred_locale/1` helper. Verified in Chrome: - `/phoenix_kit/et/users/log-in` → "Logi sisse" (Estonian) - `/phoenix_kit/users/log-in` → "Log in" / "Sign in" (English) - No flash on either route.
Closes the dialect-preference leak Codex flagged: even though the
plug + LV mount were both URL-driven for the BASE locale, all six
`DialectMapper.resolve_dialect/2` callers in `phoenix_kit_web/users/auth.ex`
still passed the current user, which let `DialectMapper`'s preferred-locale
clause upgrade the URL-derived base (e.g. "en") to a user-preferred
dialect ("en-GB") for logged-in users.
Effect under the old code: a user with `preferred_locale="en-GB"`
visiting `/phoenix_kit/admin/foo` (prefixless, primary base = "en")
got dialect "en-GB" via the dialect resolver — contradicting the
"URL is the only source of truth for routing" semantic the boss
asked for.
Fix: all six routing-path calls drop the user arg and use the
arity-1 `DialectMapper.resolve_dialect/1` (default mapping, no
preference upgrade). The dialect helper itself is unchanged — code
that legitimately wants user-preferred dialect (none in the routing
chain today) can still call the arity-2 form explicitly.
Cleanup: `get_user_for_locale_resolution/1` had no remaining callers
after the user args were removed; deleted.
Stale-comment fixes Codex flagged:
- `routes.ex:40`: the "admin paths ALWAYS get locale prefix" comment
no longer matched `build_admin_path/3`'s primary-prefixless behaviour.
- `auth.ex:1846`: matching "admin paths keep the locale in the URL"
comment in `process_valid_locale/2` updated to explain WHY the
default-locale-redirect skips admin paths today (don't canonicalise
across the still-split `:phoenix_kit_admin_locale` ↔
`:phoenix_kit_admin` live_session boundary).
- `auth.ex:1856`: matching wording on `admin_request?/1` updated.
`preferred_locale` is now WRITTEN (by the switcher hook) but read by
nothing in the routing chain. The field is preserved for future
features but functionally inert today.
`position: fixed` on the kebab menu would be interpreted relative to the nearest containing block, not the viewport. Inside a `<dialog>` on the top layer (or any ancestor with `transform`/`contain`/`filter`) this shoves the menu hundreds of pixels off-screen instead of anchoring under the trigger. Surfaced by `phoenix_kit_projects` PR BeamLabEU#9's `PopupHostLive` — assignment edit kebabs inside the modal frame rendered far right of the viewport on the host application. Reproduced locally via the parent app's `/projects-emit-demo` route. Fix portals `[data-row-menu-content]` to `document.body` in `_open()` before measuring, restores it to its original parent (tracked at `mounted()`) in `_close()` so LiveView's morphdom patching keeps finding the element on subsequent diffs. Without restoration the menu would orphan on `<body>` and LV would re-create it inside the row, doubling the DOM.
ddon
pushed a commit
that referenced
this pull request
May 19, 2026
Post-merge follow-ups on PR #551: - RowMenu: give the floating menu a stable id and add an `updated()` hook callback that drops the duplicate <ul> morphdom re-creates when a server diff hits a row whose menu is portaled to <body>. - Collapse `DialectMapper.resolve_dialect/2` to `/1` — the user-aware arity was unreachable and an active foot-gun (URL is authoritative). - Remove dead `User.preferred_locale_changeset/2` and `User.get_preferred_locale/1`; refresh docs, doctests, tests, and the now-stale `resolve_dialect/2` references in auth.ex comments. - Add `dev_docs/primary_language_no_prefix_plan.md` — the file PR #551 references in several code comments but never committed; documents TODO 1 (unify the admin live_sessions) as an implementation plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon
pushed a commit
that referenced
this pull request
May 19, 2026
PR #551 (URL-authoritative locale, prefixless admin URLs, RowMenu portal, module_assigns, publishing url_prefix fix) merged after v1.7.113 was tagged and never got a CHANGELOG entry. It ships in the 1.7.114 release, so fold its changes into that section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mdon
added a commit
to mdon/phoenix_kit
that referenced
this pull request
May 19, 2026
Lifts the locale-prefix-on-primary-language behavior from a publishing-module setting into a single site-wide toggle on the core Languages admin page (`/admin/settings/languages`). All URL generators in the workspace — `Routes.path/1`, `Routes.admin_path/2`, the sitemap, publishing's HTML builders, and publishing's canonical-redirect controller — now honor the same source of truth. ## Why PR BeamLabEU#551 made admin URLs unconditionally prefixless for the primary language. Publishing's public URLs were governed by a separate toggle (`publishing_default_language_no_prefix`, default off), and the sitemap honored neither — it always emitted `/en/blog/post` in multilang mode regardless of the setting. The boss flagged both: the setting should be site-wide, and the sitemap should respect it. ## What changed - **New core setting** `default_language_no_prefix` (boolean, default `false`) on `PhoenixKit.Modules.Languages`. Surfaced as a daisyUI toggle in a new "URL Behavior" card on the Languages admin page, above the existing "Frontend Language Switcher" card. - **`Routes.path/1` + `admin_path/2`** (`lib/phoenix_kit/utils/routes.ex`): the unconditional primary-locale strip from PR BeamLabEU#551 is now gated on `Languages.default_language_no_prefix?/0` via a new `prefixless_primary?/0` private helper. Same defensive rescue + mix-task fallback as `get_default_language_base/0`. - **Sitemap fix** (`lib/modules/sitemap/sources/publishing.ex`): `build_group_path/3` now skips the language segment for the primary language when the setting is on. Previously it always emitted the prefix in multilang mode. - **Legacy migration** (`Languages.migrate_legacy/0`): on first call, copies the value from the legacy `publishing_default_language_no_prefix` key to the new `default_language_no_prefix` key. Idempotent — once the new key is set, subsequent runs are no-ops. Pairs with the publishing- side delegate in the matching publishing PR. ## Migration safety Default is `false`, matching the historical publishing default and keeping existing installs' indexed public URLs stable on upgrade. Sites that previously toggled the publishing setting get migrated on first `migrate_legacy/0` run — they keep their explicit choice without admin action. Admin URL emission flips: PR BeamLabEU#551 emitted prefixless unconditionally for the primary locale; this PR honors the new setting (default off), so primary admin URLs grow back the `/en/` prefix unless the admin opts in. Both URL shapes still resolve at the router (dual-scope admin emission), so the only visible change is the *emitted* shape — bookmarks and external links keep working. ## Tests - `test/phoenix_kit/utils/routes_test.exs` updated to cover the new default-off behavior (no-DB rescue → primary keeps prefix). - `test/integration/languages/default_language_no_prefix_test.exs` added (DB-backed): setter round-trips, Routes helpers across both setting states, `migrate_legacy/0` no-op + backfill + idempotency. - `test/phoenix_kit_web/components/core/language_switcher_test.exs` assertions updated to match the new default. `mix test` clean (1308 tests, 0 failures, 11 doctests). `mix compile --warnings-as-errors`, `mix credo --strict`, `mix deps.unlock --check-unused`, `mix format --check-formatted` all clean. ## Follow-up surfaced (not in this PR) `phoenix_kit_entities/lib/phoenix_kit_entities/url_resolver.ex` has the same site-wide-setting blind spot in two helpers + three call sites — `build_path_with_language/3` (sitemap, always prefixes primary) and `add_public_locale_prefix/2` (public URLs, always strips primary). Surface to the entities owner before claiming site-wide unification is complete.
4 tasks
mdon
added a commit
to mdon/phoenix_kit
that referenced
this pull request
May 19, 2026
…fix` pass Three follow-up fixes surfaced by browser testing + an ast-grep sweep of all URL-emission sites: ## `process_valid_locale/2` in users/auth.ex The post-PR BeamLabEU#551 plug unconditionally 301-redirected `/<default>/...` → `/...` for non-admin paths. With the new setting OFF (the default — `/<default>/...` is canonical), the 301 was firing on every primary-language request **including form POSTs**, which silently discarded the POST body. Browser test caught this: login was completely broken with the default setting. Fixed by gating the redirect on `prefixless_primary?/0`. With setting OFF the prefixed shape is canonical and never gets stripped; with ON the strip restores the canonical-redirect behavior from PR BeamLabEU#551. ## Sitemap sources `static.ex` + `posts.ex` Both had the same bug as the publishing sitemap source I fixed in the original commit — `build_path_with_language/3` accepted an `_is_default` parameter but ignored it, emitting the prefix for the primary language in multilang mode regardless of the setting. Found by `ast-grep --lang elixir --pattern '"/#{$LOCALE}#{$PATH}"'`. Both follow the publishing source's pattern: `cond` block with explicit skip rules (nil language, single-language mode, primary + setting-on), prefix in all other cases. ## Browser verification Toggled the setting on the Languages admin page, regenerated the sitemap, and confirmed: - Setting OFF → primary entries get `/en/`, non-primary get their own prefix - Setting ON → primary entries drop `/en/`, non-primary unchanged `/en/db-test-1` 301-redirects to `/db-test-1` when setting is ON (canonical redirect intact); the same URL serves directly when setting is OFF (no redirect, POST bodies safe). ## Sweep coverage ast-grep + grep across all 14 phoenix_kit-* repos for hardcoded `/en/` segments, manual locale concatenation, raw `redirect(to:)`, hreflang/canonical emission, `~p` verified routes, og:url, and RSS/Atom feeds. No additional misses — every other URL builder either uses `Routes.path/1` (now gated) or the publishing / entities helpers (already delegated to the core setting). `mix test`: 1308 tests, 0 failures, 11 doctests. `mix compile --warnings-as-errors`, `mix credo --strict`, `mix format --check-formatted`, `mix deps.unlock --check-unused` all clean.
mdon
added a commit
to mdon/phoenix_kit
that referenced
this pull request
May 19, 2026
Surfaced by a deeper ast-grep + grep sweep across all repos for locale/language URL-emission patterns. Every other site I checked already routes through `Routes.path/admin_path` (gated) or the publishing/entities helpers (delegated). One outlier: `redirect_invalid_locale/2` (`lib/phoenix_kit_web/users/auth.ex`) strips an invalid locale segment from the URL and redirects to the cleaned path. It always emitted the *prefixless* shape, which was correct under PR BeamLabEU#551 (always-strip primary) but inconsistent with the new setting OFF (default) where the canonical primary shape is prefixed. Example: `/phoenix_kit/xx/admin/users` (xx = invalid locale) was redirecting to `/phoenix_kit/admin/users` regardless of setting. With setting OFF, the rest of the app emits `/phoenix_kit/en/admin/users`, so the redirect was landing on a non-canonical shape. Both shapes resolve at the router (dual-scope admin emission), but the inconsistency means an external link with an invalid locale lands users on a path that doesn't match the canonical URL emitted everywhere else in the app. Fixed by gating the replacement segment on `prefixless_primary?/0`: - Setting ON → strip entirely (canonical primary is prefixless) - Setting OFF → swap the invalid segment for `/<default_base>/` (canonical primary is prefixed) ## Sweep coverage (no additional misses) - All `def *path*` / `def *url*` / `def *href*` builders that take a locale param — verified to route through gated helpers. - All `String.replace(..., "/#{locale}/", ...)` sites — only used in invalid-locale stripping (above) and the now-gated canonical redirect. - All `~p` verified routes — static assets only, no locale. - All `og:url` / canonical / hreflang emitters — use `PublishingHTML.build_post_url` or `UrlResolver.public_url`, both gated. - All `<.link navigate="/admin/...">` and `<.pk_link>` — pk_link routes through `Routes.path/1`; the one bare `<.link>` hit was inside a moduledoc example, not runtime code. - Language-switcher dropdown's `resolve_url/3` → `generate_base_code_url/2` — uses `Routes.admin_path` and `Routes.path`, both gated. No remaining misses across phoenix_kit, publishing, entities, projects, ai, catalogue, newsletters, staff, comments, user_connections, legal, sync, emails, posts, locations, or hello_world. `mix test`: 1308 tests, 0 failures, 11 doctests. `mix compile --warnings-as-errors`, `mix credo --strict`, `mix format --check-formatted`, `mix deps.unlock --check-unused` all clean.
mdon
added a commit
to mdon/phoenix_kit
that referenced
this pull request
May 20, 2026
…fix` pass Three follow-up fixes surfaced by browser testing + an ast-grep sweep of all URL-emission sites: ## `process_valid_locale/2` in users/auth.ex The post-PR BeamLabEU#551 plug unconditionally 301-redirected `/<default>/...` → `/...` for non-admin paths. With the new setting OFF (the default — `/<default>/...` is canonical), the 301 was firing on every primary-language request **including form POSTs**, which silently discarded the POST body. Browser test caught this: login was completely broken with the default setting. Fixed by gating the redirect on `prefixless_primary?/0`. With setting OFF the prefixed shape is canonical and never gets stripped; with ON the strip restores the canonical-redirect behavior from PR BeamLabEU#551. ## Sitemap sources `static.ex` + `posts.ex` Both had the same bug as the publishing sitemap source I fixed in the original commit — `build_path_with_language/3` accepted an `_is_default` parameter but ignored it, emitting the prefix for the primary language in multilang mode regardless of the setting. Found by `ast-grep --lang elixir --pattern '"/#{$LOCALE}#{$PATH}"'`. Both follow the publishing source's pattern: `cond` block with explicit skip rules (nil language, single-language mode, primary + setting-on), prefix in all other cases. ## Browser verification Toggled the setting on the Languages admin page, regenerated the sitemap, and confirmed: - Setting OFF → primary entries get `/en/`, non-primary get their own prefix - Setting ON → primary entries drop `/en/`, non-primary unchanged `/en/db-test-1` 301-redirects to `/db-test-1` when setting is ON (canonical redirect intact); the same URL serves directly when setting is OFF (no redirect, POST bodies safe). ## Sweep coverage ast-grep + grep across all 14 phoenix_kit-* repos for hardcoded `/en/` segments, manual locale concatenation, raw `redirect(to:)`, hreflang/canonical emission, `~p` verified routes, og:url, and RSS/Atom feeds. No additional misses — every other URL builder either uses `Routes.path/1` (now gated) or the publishing / entities helpers (already delegated to the core setting). `mix test`: 1308 tests, 0 failures, 11 doctests. `mix compile --warnings-as-errors`, `mix credo --strict`, `mix format --check-formatted`, `mix deps.unlock --check-unused` all clean.
mdon
added a commit
to mdon/phoenix_kit
that referenced
this pull request
May 20, 2026
Surfaced by a deeper ast-grep + grep sweep across all repos for locale/language URL-emission patterns. Every other site I checked already routes through `Routes.path/admin_path` (gated) or the publishing/entities helpers (delegated). One outlier: `redirect_invalid_locale/2` (`lib/phoenix_kit_web/users/auth.ex`) strips an invalid locale segment from the URL and redirects to the cleaned path. It always emitted the *prefixless* shape, which was correct under PR BeamLabEU#551 (always-strip primary) but inconsistent with the new setting OFF (default) where the canonical primary shape is prefixed. Example: `/phoenix_kit/xx/admin/users` (xx = invalid locale) was redirecting to `/phoenix_kit/admin/users` regardless of setting. With setting OFF, the rest of the app emits `/phoenix_kit/en/admin/users`, so the redirect was landing on a non-canonical shape. Both shapes resolve at the router (dual-scope admin emission), but the inconsistency means an external link with an invalid locale lands users on a path that doesn't match the canonical URL emitted everywhere else in the app. Fixed by gating the replacement segment on `prefixless_primary?/0`: - Setting ON → strip entirely (canonical primary is prefixless) - Setting OFF → swap the invalid segment for `/<default_base>/` (canonical primary is prefixed) ## Sweep coverage (no additional misses) - All `def *path*` / `def *url*` / `def *href*` builders that take a locale param — verified to route through gated helpers. - All `String.replace(..., "/#{locale}/", ...)` sites — only used in invalid-locale stripping (above) and the now-gated canonical redirect. - All `~p` verified routes — static assets only, no locale. - All `og:url` / canonical / hreflang emitters — use `PublishingHTML.build_post_url` or `UrlResolver.public_url`, both gated. - All `<.link navigate="/admin/...">` and `<.pk_link>` — pk_link routes through `Routes.path/1`; the one bare `<.link>` hit was inside a moduledoc example, not runtime code. - Language-switcher dropdown's `resolve_url/3` → `generate_base_code_url/2` — uses `Routes.admin_path` and `Routes.path`, both gated. No remaining misses across phoenix_kit, publishing, entities, projects, ai, catalogue, newsletters, staff, comments, user_connections, legal, sync, emails, posts, locations, or hello_world. `mix test`: 1308 tests, 0 failures, 11 doctests. `mix compile --warnings-as-errors`, `mix credo --strict`, `mix format --check-formatted`, `mix deps.unlock --check-unused` all clean.
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Locale routing, admin URL shapes, and the LayoutWrapper attr contract — three intertwined fixes that have to land together so the matching publishing PR can rely on the new core API.
What's in this PR
1. Admin URLs render prefixless for the primary language (
5b9853ff)Routes.admin_path/2and the internalbuild_admin_path/3now strip the locale segment when it matches the default — mirrors whatbuild_localized_path/3already does for non-admin routes./phoenix_kit/admin/usersAND/phoenix_kit/en/admin/userswork; the dual-scope admin route emission keeps the legacy prefixed shape addressable.dev_docs/primary_language_no_prefix_plan.md(TODO 1).test/phoenix_kit/utils/routes_test.exspins both helper shapes with backcompat coverage.2. URL is the only source of truth for locale routing (
fecf3d3e,85f11427,c836286e)mount_phoenix_kit_current_scope/3) and the HTTP plug. The previous fallback chain (URL → session → default) caused a sticky-locale trap: visiting/foo/et/...once stashed"et"in the session, then every prefixless URL inherited Estonian — refreshing didn't help.preferred_localeis no longer read for routing (base OR dialect). The field is still WRITTEN by the language-switcher hook so future features can re-enable it; it just doesn't influence routing today.get_user_for_locale_resolution/1(dead after the changes) andget_user_preferred_locale/1.renew_session/1no longer preserves the dead session key across login/logout.DialectMapper.resolve_dialect/2callsites inphoenix_kit_web/users/auth.exswitched to the arity-1 variant so dialect resolution doesn't sneakpreferred_localeback in either.3.
LayoutWrapper.app_layoutgeneralized via:module_assigns(b17b96b7):phoenix_kit_publishing_translationsattr with a generic:module_assignsmap. Each key in the map is merged into the top-level assigns (withMap.put_new— core keys win) before the parentLayouts.appis called.:phoenix_kit_publishing_translationsand:og.Verification
mix test test/phoenix_kit/utils/routes_test.exs— 10/0mix test test/phoenix_kit/languages/dialect_mapper_test.exs— 24/0/phoenix_kit/et/users/log-inrenders Estonian,/phoenix_kit/users/log-inrenders English; no flash, no sticky locale, plug and LV agree.Coordination
This PR pairs with phoenix_kit_publishing #TBD which uses the new
:module_assignsshape. Either lands first as long as both land — they're functionally additive.Test plan
/phoenix_kit/admin/...and/phoenix_kit/<locale>/admin/...— both resolve/foo/et/...then a prefixless URL — locale flips back to default (no sticky session)