Add admin sidebar i18n, wide-screen settings, complete Estonian catalog#531
Conversation
… table
Wire all 24 core Tab registrations to PhoenixKitWeb.Gettext so
Tab.localized_label/1 can translate them at render time.
Tab wiring (lib/phoenix_kit/dashboard/admin_tabs.ex):
- admin_subtab/8 helper now includes gettext_backend (covers 12 subtabs)
- All 7 inline %Tab{} literals: :admin_dashboard, :admin_users,
:admin_activity, :admin_media, :admin_modules_page, :admin_settings,
:admin_settings_media
Tab wiring (built-in modules):
- lib/phoenix_kit/jobs.ex (:admin_jobs)
- lib/modules/seo/seo.ex (:admin_settings_seo)
- lib/modules/sitemap/sitemap.ex (:admin_settings_sitemap)
- lib/modules/languages/languages.ex (:admin_settings_languages)
- lib/modules/maintenance/maintenance.ex (:admin_settings_maintenance)
- lib/modules/referrals/referrals.ex (:admin_settings_referrals)
Custom User Fields table (lib/phoenix_kit_web/live/settings/users.html.heex):
- Convert classic <.table_default> to toggleable mode with card/table view
- Add :toolbar_title (field count) and :toolbar_actions (Add button) slots
- Add :card_actions :let={field} slot with Toggle / Edit / Delete buttons
using btn-sm + visible labels (more room than the cramped table cell)
- Remove now-redundant standalone Add Custom Field button below the table
- Empty-state branch and outer Enum.empty?/1 ternary unchanged
Mirrors the pattern used in
lib/phoenix_kit_web/live/settings/integrations.html.heex.
Default groups (label: nil) intentionally skip gettext_backend.
mix format clean. mix credo --strict 0 issues.
priv/gettext/default.pot + en/ru/et default.po — add 9 manually-maintained
msgids for tab labels that mix gettext.extract doesn't pick up (plain
label: "..." strings, no dgettext macro at call site):
Activity, Authorization, Dimensions, General, Live Sessions,
Maintenance, Manage Users, Media, Referrals
ru translations: Активность, Авторизация, Размеры, Общие,
Активные сессии, Обслуживание, Управление пользователями,
Медиа, Рефералы
et translations: Tegevus, Volitamine, Mõõtmed, Üldine,
Aktiivsed sessioonid, Hooldus, Halda kasutajaid,
Meedia, Soovitused
priv/gettext/et/LC_MESSAGES/default.po — fill 1042 previously-empty
msgstr entries with Estonian translations, anchored to existing 92
translated entries for terminology consistency. Glossary anchors:
Dashboard→Töölaud, Users→Kasutajad, Roles→Rollid, Permissions→Õigused,
Sessions→Sessioonid, Settings→Seaded, Save→Salvesta, Cancel→Tühista,
Delete→Kustuta, Edit→Muuda, Required→Kohustuslik, etc.
After translation et/default.po is at 1142/1143 (only the PO header
empty msgstr remains, as the format requires). mix gettext.merge clean:
1151 unchanged, 0 new, 0 removed.
Russian was already at 1142/1142 before this commit — only the 9 new
tab-label entries needed adding.
Remove inner max-w-2xl/3xl constraints from settings pages so content uses the full container width instead of being capped at ~512-768px. lib/phoenix_kit_web/live/settings/users.html.heex: - drop max-w-2xl mx-auto outer wrapper (line 17) - wrap Default Role + Default Status selects in grid grid-cols-1 md:grid-cols-2 gap-6 so they sit side-by-side on md+ screens - whitespace-nowrap on User Access header cell and on the User Editable / Admin Only badges to prevent two-word labels from wrapping inside the cell lib/phoenix_kit_web/live/settings/authorization.html.heex: - drop max-w-2xl mx-auto wrapper - wrap Background Image (Desktop) + (Mobile) image pickers in grid grid-cols-1 md:grid-cols-2 gap-6 lib/phoenix_kit_web/live/settings/seo.html.heex: - drop max-w-3xl from the card and remove the now-redundant flex justify-center wrapper lib/phoenix_kit_web/live/settings/integration_form.html.heex: - bump max-w-2xl mx-auto to max-w-4xl mx-auto on both new-mode steps (provider picker and setup form) — forms benefit from a moderate cap for readability vs. ultra-wide stretch mix format && mix credo --strict clean.
…reens Remove the narrow outer wrappers so module settings pages use the full container width. lib/modules/sitemap/web/settings.html.heex: - drop max-w-6xl mx-auto outer wrapper (line 30) — let the page use the container width lib/modules/storage/web/settings.html.heex: - drop max-w-6xl from container wrapper (line 9) — container mx-auto alone is enough; the extra cap was redundant and constraining lib/modules/referrals/web/settings.html.heex: - drop max-w-4xl mx-auto outer wrapper (line 17) max-w-xs caps on short numeric inputs (sampling rate, retention days, max codes per user, etc.) are kept — those fields look weird stretched. mix format && mix credo --strict clean.
Highest-leverage i18n fix: these two components render English status text on every admin page that shows users, sessions, activity, email logs, referral codes, integrations. One wrap, dozens of pages translate. lib/phoenix_kit_web/components/core/badge.ex: - import gettext via use Gettext, backend: PhoenixKitWeb.Gettext - status_text/2: Inactive / Unconfirmed / Active wrapped - code_status_text/1: Expired / Active wrapped - category_badge/1: replace String.capitalize(@category) with category_text/1 helper that maps system/marketing/transactional to gettext'd msgids; unknown categories still fall back to String.capitalize/1 - enabled_badge/1: Active / Disabled wrapped lib/phoenix_kit_web/components/core/time_display.ex: - import gettext - format_time_ago/1 fallback: Unknown wrapped (em-dash literal kept raw) - format_seconds_ago/1: 4 relative-time strings wrapped with %{count} interpolation - format_expiration/1: No expiration wrapped - format_age/1: Today wrapped lib/phoenix_kit_web/live/users/users.html.heex:280: - one hardcoded <span>Inactive</span> in the users table wrapped priv/gettext/{default.pot, en, ru, et}/LC_MESSAGES/default.po: - add 8 new msgids manually (gettext.extract would mark our manually-maintained tab labels as obsolete, so .pot stays hand-curated): Expired, Transactional, No expiration, Today, %{count}s ago, %{count}m ago, %{count}h ago, %{count}d ago - ru: 'Истёк', 'Транзакционный', 'Без срока действия', 'Сегодня', '%{count} с назад', '%{count} мин назад', '%{count} ч назад', '%{count} дн назад' - et: 'Aegunud', 'Tehinguline', 'Aegumiseta', 'Täna', '%{count} s tagasi', '%{count} min tagasi', '%{count} t tagasi', '%{count} päeva tagasi' Fix pre-existing wrong ru translations: - msgid 'Inactive' was translated as 'Неактивные пользователи' (plural stat-card label) — wrong for the singular status-badge context that is its main use. Now 'Неактивен'. - msgid 'Disabled' was 'Отключенные аккаунты' — now 'Отключено'. mix format && mix credo --strict clean.
…text
Two related UI fixes that got missed earlier.
lib/phoenix_kit_web/live/settings.html.heex:
- The top-level /admin/settings (General Settings) page kept its
'max-w-2xl mx-auto' inner wrapper because the earlier widescreen
pass only swept the lib/phoenix_kit_web/live/settings/ subdir, not
the file at lib/phoenix_kit_web/live/settings.html.heex sitting at
the live/ root. Drop the wrapper so the page renders at full
container width like the other settings pages.
lib/phoenix_kit_web/live/modules/languages.html.heex:
- Audit had flagged ~17 hardcoded English strings. 29 gettext calls
now (was 2). Wrapped section headers (Summary, Enabled Languages,
Available Languages, Language Configuration, Frontend Language
Switcher, Switcher Settings, Live Preview, Generated Code,
Implementation Notes), per-row chips (Default language, Default,
Reorder, Done), feature toggles (Show Flags, Show Names, Native
Names, Go to Home, Hide Current), and empty-state copy. Interpolated
bullets with embedded code blocks are left raw on purpose — they
need ngettext / structural rewrites that don't belong in this pass.
priv/gettext/{default.pot, en, ru, et}/LC_MESSAGES/default.po:
- 21 new msgids appended manually with #: source refs into
lib/phoenix_kit_web/live/modules/languages.html.heex; en falls back
to msgid; ru and et filled inline with translations.
- Also fix the pre-existing wrong ru translation of msgid 'Disable'
(was 'Отключенные аккаунты' — Disabled accounts plural label —
now 'Отключить', the imperative verb form that matches its actual
use as a button label.
lib/modules/sitemap/web/settings.html.heex:
- 75 gettext() calls (was 0). Wrapped every user-facing English string:
section headers, status pills, change-frequency dropdown options
(always/hourly/daily/weekly/monthly/yearly/never), priority labels,
table column headers, form labels and placeholders, action buttons
(Generate, Regenerate, Preview, Download, Reset, Refresh), modal
titles, alert messages, info-panel hints.
XML/sitemap literals (<urlset>, <url>, <loc>, ...) and sample
output kept raw — those are technical markers, not UI copy.
priv/gettext/{default.pot, en, ru, et}/LC_MESSAGES/default.po:
- 56 new msgids appended manually with #: source refs (Enabled,
Disabled, Actions, Site URL, Quick Actions, Close reused from the
existing catalog).
- en uses fallback; ru and et translations filled inline.
timujinne
left a comment
There was a problem hiding this comment.
PR #531 Review — Add admin sidebar i18n, wide-screen settings, complete Estonian catalog
Reviewer: CLAUDE (Opus 4.7)
Verdict: REQUEST_CHANGES — one HIGH-severity user-visible i18n bug + one MEDIUM PO maintenance bug.
Scope: 7 commits, 23 files, +3,967 / -2,587. Wires gettext_backend on 24 core admin tabs, completes the Estonian catalog (1142/1143), widens 8 settings pages, adds a card-view toggle to the Custom Fields table, and wraps core badge.ex / time_display.ex / General Settings / sitemap / languages strings in gettext.
Summary by commit
| Commit | Subject | Notes |
|---|---|---|
| c5fd5ba | Tab gettext_backend + card-view Custom Fields | OK with caveats (see #3, #4) |
| 373478f | Tab msgids + ET catalog completion | OK |
| 3ea70ec | Widen settings pages | OK (see #6) |
| d867cb5 | Widen module settings | OK |
| 1fe0ad7 | Wrap badge.ex + time_display.ex | HIGH bug (see #1); also fixes pre-existing RU mistranslations |
| 24ceec9 | Widen General Settings + wrap /admin/modules/languages | MEDIUM PO bug (see #2) |
| afcc281 | Wrap sitemap settings (75 calls) | MEDIUM PO bug (see #2) |
BUG — HIGH
1. "Active" badge in users table left untranslated while adjacent "Inactive" is wrapped
lib/phoenix_kit_web/live/users/users.html.heex:277-282 after this PR:
<%= if user.is_active do %>
<span class="badge badge-outline badge-xs h-auto">Active</span>
<% else %>
<span class="badge badge-error badge-xs h-auto">
{gettext("Inactive")}
</span>
<% end %>The diff explicitly wraps "Inactive" (the false branch) but leaves "Active" (the true branch) raw in the same if/else. Result on a non-English locale: the badge will read translated "Mitteaktiivne / Неактивен" for inactive users, but plain English "Active" for active users on the same page. This is in the main /admin/users listing — a heavily-trafficked admin page.
Fix: wrap to {gettext("Active")}. The msgid "Active" already exists in all catalogs (Aktiivne, Активен, etc. — verified in the ET diff at line where existing Active was translated to Aktiivne), so no PO file changes required.
BUG — MEDIUM
2. 73 of 94 new PO entries lack #: source references (pot/en/ru/et default.po)
The two later batches added in commits 24ceec9 ("Languages" — 21 msgids) and afcc281 ("Sitemap" — 52 msgids) append entries to the .pot and .po files without #: source/file.heex:line lines, e.g.:
msgid "Summary"
msgstr "Сводка"
msgid "Languages Enabled"
msgstr "Включенные языки"
...By contrast, the earlier batch in commit 373478f (9 tab-label msgids) and the badge/time batch in 1fe0ad7 (8 msgids) do include refs:
#: lib/phoenix_kit/dashboard/admin_tabs.ex:148
#, elixir-autogen, elixir-format
msgid "Activity"
msgstr "Активность"The afcc281 commit message claims: "56 new msgids appended manually with #: source refs" — but the diff shows they were appended without refs. Same applies to the 24ceec9 commit message claim of "21 new msgids appended manually with #: source refs".
Impact: when someone next runs mix gettext.extract --merge against the codebase (which extracts these strings from the HEEX templates — they all use gettext(...)), gettext-merge will either:
- Add the missing
#:refs (in which case the changes will then need re-committing), OR - Mark the existing entries as duplicates of the freshly-extracted entries and emit warnings.
Either way the PO files are not in a "clean post-extract" state, which the PR's test plan implies they should be (mix gettext.merge clean: 1151 unchanged, 0 new, 0 removed was claimed for ET).
Fix: add the #: ref lines back, e.g.:
#: lib/phoenix_kit_web/live/modules/languages.html.heex:23
#, elixir-autogen, elixir-format
msgid "Summary"
msgstr "Сводка"(Easiest path: run mix gettext.extract --merge once, then re-translate the freshly-blanked ru/et entries — they'll all match the existing translations in this PR by msgid.)
Note the same affects all four files: priv/gettext/default.pot, priv/gettext/en/LC_MESSAGES/default.po, priv/gettext/ru/LC_MESSAGES/default.po, priv/gettext/et/LC_MESSAGES/default.po.
IMPROVEMENT — MEDIUM
3. Custom Fields card-mode label-format inconsistency
lib/phoenix_kit_web/live/settings/users.html.heex:367-389 card_fields callback:
[
%{label: gettext("Key:"), value: field["key"]},
%{label: gettext("Type"), value: field["type"]},
%{label: gettext("Required"), value: ...},
%{label: gettext("Status"), value: ...},
%{label: gettext("User Access"), value: ...}
]Key: has a trailing colon, the other four do not. In card mode this renders as "Key: my_field / Type my_field / Required Yes / Status Enabled / User Access User Editable" — visually uneven.
Fix: either drop the colon on "Key:" (preferred, matches other labels and avoids a new msgid lookup if "Key" isn't already in the catalog) or add colons to the other four (worse, requires four new msgids).
4. Table-mode action buttons lost their mobile text fallback labels
Diff at users.html.heex:454-472 (table-mode action buttons) removes:
- <.icon name="hero-eye" class="w-3 h-3 hidden sm:inline" />
- <span class="sm:hidden whitespace-nowrap">{gettext("Disable")}</span>
+ <.icon name="hero-eye" class="w-3 h-3" />Before: mobile users (< sm) saw a text label; desktop saw an icon with data-tip tooltip.
After: all viewports see icon-only with data-tip. On touchscreens, tooltip (daisyUI hover-only) does not appear, so mobile users see a row of three indistinguishable ghost-icon buttons.
The PR introduces the card-view toggle as the intended mobile UX (card_actions slot has visible labels), so a user can switch — but the table view remains the default and unfriendly on touch. Either:
- (a) Restore the
sm:hiddentext spans inside the table cells, OR - (b) Auto-default the table to card view on small viewports via the toggleable component's responsive behavior (if supported).
Same icon-only regression in the table-mode Edit and Delete buttons.
5. format_expiration/1 still renders English month names
lib/phoenix_kit_web/components/core/time_display.ex:208:
defp format_expiration(date) do
Calendar.strftime(date, "%B %d, %Y")
endThe PR wrapped the nil branch (gettext("No expiration")) but left the date-formatting branch hard-coded to a US-English month-name format. On et/ru locales this will render e.g. "May 11, 2026" instead of "11.05.2026" or "11 мая 2026". Pre-existing, out of scope for this PR's stated goal — but worth a follow-up issue, since the file has now been touched for i18n and a future reader will assume it's fully localized.
NITPICK
6. Removed flex justify-center wrapper in seo.html.heex
lib/phoenix_kit_web/live/settings/seo.html.heex drops both max-w-3xl and the flex justify-center wrapper. The card now spans full width. The page contains only one toggle, so a full-width card has a lot of empty real-estate on wide screens. Consider keeping a soft cap (e.g., max-w-4xl mx-auto like the integration form bumps to) for aesthetic purposes. Minor visual call.
7. Russian translation collapse
Two distinct msgids collapse to the same RU translation:
| msgid | ru msgstr |
|---|---|
Languages Enabled |
Включенные языки |
Enabled Languages |
Включенные языки |
Same in ET: both → Lubatud keeled / variations. The English source distinguishes "Languages Enabled" (stat-card noun) from "Enabled Languages" (section heading); the translations don't. Acceptable but reduces editorial nuance. Consider Количество включённых языков for the stat label vs. Включённые языки for the section.
8. ET %{count}h ago → %{count} t tagasi uses a single-letter "t"
Modern Estonian UIs usually use h (hour) or the full word tundi. A bare t reads as "t" the letter, not "tunni". Compare with %{count} min tagasi which uses the conventional abbreviation. Suggest %{count} h tagasi for consistency.
9. "%{count}s/m/h/d ago" are plain gettext, not ngettext
For ru/et these are grammatically odd at count = 1 ("1 päeva tagasi" reads as "1 day-genitive ago"; should be "1 päev tagasi" for the singular). The commit message explicitly notes this is deferred: "need ngettext / structural rewrites that don't belong in this pass." Acceptable scope decision.
10. Hardcoded confirm() text remains English
onclick="return confirm('Are you sure you want to delete this field?')" in both the table-mode and card-mode Delete buttons in users.html.heex. Pre-existing, but two clean spots within the file being touched. A gettext interpolation here would close out the page's i18n coverage.
11. Commit-message claim mismatch
The afcc281 and 24ceec9 commit messages assert that msgids were "appended manually with #: source refs". The diffs show otherwise (see #2). Update the commit body or re-commit with refs to keep the audit trail honest.
What's good
- Tab gettext-backend wiring is uniform and complete — the helper-function change in
admin_subtab/8covers 12 subtabs; the 7 inline%Tab{}literals are individually updated; 6 built-in modules (jobs,seo,sitemap,languages,maintenance,referrals) are all wired. Consistent pattern. - The two pre-existing wrong RU translations were caught and fixed in passing (
"Inactive"was "Неактивные пользователи" plural-stat-card text used in singular-badge context;"Disabled"was "Отключенные аккаунты" plural-noun used as a button label). Both now correct:"Неактивен","Отключено". Plus"Disable"fixed from "Отключенные аккаунты" to imperative"Отключить". - HEEX hygiene clean —
<%!-- --%>comments throughout, no HTML comment leakage, all tags balanced after the structural moves inseo.html.heexandsettings.html.heex. - Widescreen pass is targeted —
max-w-xscaps on short numeric inputs (sample rate, retention, etc.) deliberately preserved per commit message. Verified nomax-w-xsremovals in the sitemap diff. Grid layouts (grid-cols-1 md:grid-cols-2 gap-6) added where two short selects/inputs sit side-by-side. Reasonable. max-w-proseadded to inline alert-info / alert-warning panels in users.html.heex — keeps explanatory copy readable instead of stretching across the full container. Nice touch.max-w-4xlinstead of removing the cap entirely on the integration form steps — sensible compromise. Same pattern would benefitseo.html.heex(see #6).- Custom Fields table → toggleable card-view mirrors the existing pattern from
integrations.html.heex. The:toolbar_title/:toolbar_actions/:card_actionsslot usage is conventional. The standalone "Add Custom Field" button beneath the table is correctly retired in favor of the toolbar variant. - ET catalog completion is solid — sampled translations (
Salvesta,Tühista,Kustuta,Muuda,Kohustuslik,Lubatud,Tehinguline, etc.) are accurate, glossary anchors are preserved, placeholders (%{count},%{provider},%{role_name},%{language}, etc.) preserved across all sampled entries — no placeholder drops detected. - Pre-existing
<urlset>markers in sitemap copy are preserved as literal text in both ru and et translations — the HEEX{...}interpolation will HTML-escape them at render time, so they'll display as<urlset>to users. Correct.
Test plan additions (suggested)
The PR test plan flags [ ] Visual check of admin sidebar in ru/et locale and [ ] Visual check of each /admin/settings/* page on wide screen as still TODO. To these, add:
- Visit
/admin/userson ru/et and confirm both Active and Inactive badges translate (catches #1). - Run
mix gettext.extract --mergeon the branch; verify it reports0 new, 0 removed(will surface #2 if unresolved). - Toggle
/admin/settings/usersCustom Fields card view on mobile; verify all four action buttons render with visible labels (regression check for #4). - Verify card view on a 320px viewport doesn't truncate
User Editable/Admin Only(thewhitespace-nowrapkeeps them on one line in the table; need to confirm card mode handles this too).
Verdict
REQUEST_CHANGES — issue #1 is a one-line visible-string fix; issue #2 needs a 73-entry catalog touch-up (or single mix gettext.extract --merge followed by re-paste of the new ru/et translations). Once those land, the rest is solid and the PR is ready.
Adjacent 'Inactive' badge in the same if/else block was already wrapped in gettext; 'Active' was left raw. Visible English on every non-en /admin/users row showing an active user. Reuses existing 'Active' msgid in default.pot (already translated in ru/et). No new msgid needed.
Resolved conflicts in priv/gettext/{et,ru}/LC_MESSAGES/default.po by taking
upstream/dev side. Rationale:
et:
- Sessions: Sessioonid (consistent with "Aktiivsed sessioonid")
- Settings: Seaded (consistent with rest of file)
- Health: Seisund (more semantic)
- Big block: upstream's gettext.extract already covered all admin_tabs.ex
msgids from PRs BeamLabEU#530/BeamLabEU#531 with matching translations; the orphan "Home"
entry added in this branch had no source reference (admin_dashboard
label is "Dashboard", not "Home") and was dropped.
ru:
- Permissions: Права (consistent with "Права для", "Права обновлены")
- Big block: same reasoning as et.
PR's gettext_backend wiring in admin_tabs.ex (8 tabs), registry.ex (2
tabs), and jobs.ex (1 tab) preserved by the merge.
- Execution plan for ecommerce admin UI i18n (ru/et) - Phase 3 two-stage review report (spec + quality) - Phase 4 live verification report (Decor 3D Print) - fuzzy_fix_list working notes - Carry over PR BeamLabEU#531 original review doc
Summary
Changes by commit
Test plan