Skip to content

Add :per_translation_urls attr to core LanguageSwitcher - #525

Merged
ddon merged 4 commits into
BeamLabEU:devfrom
mdon:feat/language-switcher-host-integration
May 8, 2026
Merged

Add :per_translation_urls attr to core LanguageSwitcher#525
ddon merged 4 commits into
BeamLabEU:devfrom
mdon:feat/language-switcher-host-integration

Conversation

@mdon

@mdonmdon commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Lets a feature module supply explicit per-translation URLs that
override the locale-rewrite default in
PhoenixKitWeb.Components.Core.LanguageSwitcher. Pairs with
BeamLabEU/phoenix_kit_publishing#15.

Why

The core switcher's locale-rewrite default works when languages
share a URL structure that differs only by the :locale segment.
It breaks when a feature module computes per-translation canonical
URLs that aren't simple substitutions — most notably publishing's
per-language URL slugs, where /en/blog/my-post and
/fr/blog/mon-article aren't related by segment swap.

Publishing already builds the right per-translation URLs for its
in-page switcher and now exposes them on the conn under
:phoenix_kit_publishing_translations (in the matching publishing
PR). The host's root layout passes that list to the core switcher
via the new :per_translation_urls attr, and each language renders
with its publishing URL instead of the locale-rewrite default.

<.language_switcher_dropdowncurrent_locale={@current_locale}current_path={@url_path}per_translation_urls={assigns[:phoenix_kit_publishing_translations]}/>

When the assign is nil (non-publishing pages), the switcher falls
back to its existing default — fully backward compatible.

How

A new private resolve_url/3 helper replaces the eight
generate_base_code_url/2 call sites in the dropdown / buttons /
inline templates. It looks up the language's base_code against the
supplied per_translation_urls list (atom or string keyed —
publishing's atom-keyed shape is the typical caller), normalizing
full dialect codes ("en-US") to their base ("en") via
DialectMapper.extract_base/1. On a miss it falls through to
generate_base_code_url/2 per-language, so URLs without a
publishing entry continue to use the default.

Backward compatibility

The new attr defaults to nil, so any existing caller that doesn't
pass it gets identical pre-feature behavior. No host integration is
required to keep current installs working.

Verification

mix format --check-formatted # clean
mix credo --strict # clean
mix test # 1112 tests, 5 failures
(all pre-existing — V107 migration tests +
a permissions test, unrelated to this change)

Verified pre-existing nature by stash-and-rerun against the diff.
The 7 new tests in
test/phoenix_kit_web/components/core/language_switcher_test.exs
all pass; previously there was no test file for
Components.Core.LanguageSwitcher.

Test plan

  • mix precommit clean
  • 7 new tests cover atom-keyed and string-keyed input lists, the
    full-dialect-to-base-code normalization, fallback per-language
    when an entry isn't in the list, nil/empty/missing-attr
    pass-through to the historical default
  • No behavior change when per_translation_urls isn't passed

Coordination

Doesn't strictly require the matching publishing PR to ship —
hosts that don't use publishing get backward-compatible behavior
(attr defaults to nil, no override). The publishing PR is what
populates the conn assign that hosts can then thread through.

Files

  • lib/phoenix_kit_web/components/core/language_switcher.ex (3 new attrs across the dropdown/buttons/inline variants, 1 new private resolve_url/3 helper, 8 call-site replacements, 2 helpers entry_base_code/1 and entry_url/1)
  • test/phoenix_kit_web/components/core/language_switcher_test.exs (NEW, 7 tests)

mdon added 4 commits May 9, 2026 00:36
… view
Containers that set `data-sortable-handle="<selector>"` on the SortableGrid
hook now restrict drag initiation to descendants matching the selector
(SortableJS `handle:` option). Unset → unchanged whole-item drag.
`<.table_default>`'s mobile card view opts in by default when
`:on_reorder` is set: emits `data-sortable-handle=".pk-drag-handle"`,
adds the `pk-drag-handle` class to the existing footer drag-handle div,
and drops `cursor-grab` from the card body. Clicks, text selection, and
button presses on the card body now pass through normally.
SortableGrid hook snapshots each <td>'s offsetWidth into an inline
style on `onChoose` and restores on `onUnchoose`. forceFallback +
fallbackOnBody clones the dragged <tr> to <body>, where it loses its
<table> ancestor and every cell collapses to its content width — the
floating row visually flattened to the left. Pinning widths inline
keeps the column layout intact while the drag is in flight.
table_default's card-view footer reorganised so the drag handle stays
leftmost while action buttons cluster on the right via a wrapper with
ml-auto. Pre-fix used `card-actions justify-between` with an isolated
handle div — when the action buttons wrapped on narrow cards, the
handle ended up alone on row 1 above an empty space and the buttons
on row 2, visually disconnected. Now both share a flex-wrap row;
when buttons must wrap, the handle and buttons stay clustered.
The hook listens for a `sortable:flash` push_event the LV emits after
each reorder attempt — `{uuid, status: "ok" | "error"}` — and applies
a transient highlight class to every DOM element with the matching
data-id (so both the table row and the card view counterpart light up
together).
Implementation uses a `::after` pseudo-element overlay rather than
animating background-color directly. Direct bg animation overrode
existing card backgrounds (e.g. bg-base-200) and bled the page bg
through during the keyframe interpolation; the overlay sits on top of
whatever bg the row/card already has, so cards keep their grey while
the green/red flash fades in and out.
`moved_id` is now included on every reorder payload (was previously
only on cross-container drops) so the LV can flash any reorder, not
just cross-container moves.
Lets a feature module (currently `phoenix_kit_publishing`) supply
explicit per-translation URLs that override the locale-rewrite
default in the dropdown / buttons / inline variants of the core
switcher. Useful when the consumer has computed canonical URLs for
each available translation that the simple locale-rewrite default
can't reproduce — for example when a post has per-language URL
slugs.
The new attr accepts a list of `%{code: <display_code>, url:
<full_url>}` maps (atom or string keys). The internal `resolve_url/3`
helper picks each language's URL by matching the entry's `code`
(normalized to its base via `DialectMapper.extract_base/1`) against
the language's `base_code`, falling back to the existing
`generate_base_code_url/2` default when no entry matches.
Host root layouts pass
`per_translation_urls={assigns[:phoenix_kit_publishing_translations]}`
— when publishing's controller assigned the conn, the switcher
shows publishing's URLs; when not (non-publishing pages), the assign
is `nil` and the switcher falls back to default behavior.
Pairs with `phoenix_kit_publishing` feature branch
`feat/language-switcher-host-integration` which exposes the conn
assign + adds the `publishing_show_language_switcher` setting that
suppresses publishing's own in-page switcher when the host already
provides one.
Tests: 7 new tests in
test/phoenix_kit_web/components/core/language_switcher_test.exs
covering the URL override (atom + string keyed), full-dialect to
base-code normalization, fallback per-language when an entry isn't
in the list, nil/empty/missing-attr pass-through to the historical
default behavior.
@ddon
ddon merged commit e48a8f9 into BeamLabEU:devMay 8, 2026
ddon added a commit that referenced this pull request May 8, 2026
Eight post-merge reviews covering V111 PDF tables, the DB-module
extraction, MediaBrowser modal + LV login return_to, external-plugin
admin permissions, dashboard sidebar gettext, live Hex.pm known-packages
fetch, the publishing routing-strategy shim, and the LanguageSwitcher
per_translation_urls attr (plus the bundled DnD work).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon added a commit that referenced this pull request May 8, 2026
…525
Code/doc fixes addressing one finding per PR (or several where
trivial). Each closes a NITPICK or IMPROVEMENT-LOW from the
matching CLAUDE_REVIEW.md; design-level / breaking / risky items
deferred per the FOLLOW_UP.md "Skipped" sections.
- #516: Drop dead `String.to_atom` fallback in OAuth interpolate_url
- #518: Delete stray 0-byte pages_html.ex
- #519: Fix stale `viewer={true}` template comment + login_path
trailing-slash self-loop guard
- #521: Resolution-order doc on permission_key_for_admin_view/1
- #522: Hot-reload safety pitfall in per-module-i18n.md
- #523: KnownPackages — max-pages cap, ensure_table race comment,
Logger-levels operational signals in moduledoc
- #524: __mix_recompile__?/0 note next to apply/3 explanation
- #525: LanguageSwitcher attr doc atom/string keys + DRY resolve_url
per-language + JS sortable:flash defensive status check
Plus FOLLOW_UP.md per PR enumerating closed vs deferred items.
PR #525's FOLLOW_UP also captures the bundled DnD audit trail
(table_default drag-handle scoping, sortable:flash, TR cell-width
preservation) absent from the original PR body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ddon added a commit that referenced this pull request May 8, 2026
Three-day window of accumulated work since 1.7.105 (2026-05-05):
PRs #516, #518, #519, #521, #522, #523, #524, #525, plus the
review-doc suite and post-merge triage closing nitpicks across all
eight.
Headline changes — V111 PDF library tables, DB module extracted to
phoenix_kit_db, MediaBrowser modal viewer, sidebar gettext API, live
Hex.pm catalog, publishing routing-strategy shim closing the
/:locale/<literal>/... host-route shadowing bug, LanguageSwitcher
:per_translation_urls, and bundled DnD improvements (drag-handle
scoping, sortable:flash, TR cell-width preservation).
All changes are strictly additive / non-breaking; one transitional
extraction (DB → phoenix_kit_db) requires the paired Hex package
once it ships.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@mdon@ddon