From ae663a745be15a7d0a026d1f5ca1ead6715998d4 Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Sat, 25 Jul 2026 15:01:02 +0300 Subject: [PATCH] Improve localized notifications and local time UX --- assets/js/hooks.js | 25 +- e2e/tests/notifications-data.spec.ts | 3 + lib/who_need_help/help.ex | 6 +- lib/who_need_help/notifications/text.ex | 153 +++ lib/who_need_help/push.ex | 8 +- .../push/device_delivery_worker.ex | 13 +- .../push/notification_dispatch_worker.ex | 6 +- .../push/notification_email_worker.ex | 6 +- lib/who_need_help/trust.ex | 6 +- .../components/core_components.ex | 33 + .../controllers/support_html.ex | 6 - .../controllers/support_html/show.html.heex | 22 +- .../live/activity_live/index.ex | 10 +- .../live/activity_live/show.ex | 19 +- .../live/notification_live.ex | 199 ++-- lib/who_need_help_web/live/report_live.ex | 15 +- .../live/request_live/index.ex | 12 +- .../live/request_live/show.ex | 23 +- priv/gettext/default.pot | 944 ++++++++++-------- priv/gettext/en/LC_MESSAGES/default.po | 944 ++++++++++-------- priv/gettext/ru/LC_MESSAGES/default.po | 944 ++++++++++-------- priv/gettext/uk/LC_MESSAGES/default.po | 944 ++++++++++-------- .../who_need_help/notifications/text_test.exs | 60 ++ .../live/mutual_aid_live_test.exs | 19 +- 24 files changed, 2711 insertions(+), 1709 deletions(-) create mode 100644 lib/who_need_help/notifications/text.ex create mode 100644 test/who_need_help/notifications/text_test.exs diff --git a/assets/js/hooks.js b/assets/js/hooks.js index 86de83b..93bde58 100644 --- a/assets/js/hooks.js +++ b/assets/js/hooks.js @@ -345,7 +345,18 @@ const createAidMap = element => { content.append(title) const location = document.createElement("small") - location.textContent = point.location || "" + const capturedAt = point.captured_at ? new Date(point.captured_at) : null + const capturedAtLabel = + capturedAt && !Number.isNaN(capturedAt.getTime()) + ? new Intl.DateTimeFormat(undefined, { + dateStyle: "short", + timeStyle: "medium" + }).format(capturedAt) + : "" + location.textContent = + capturedAtLabel + ? `${point.location_prefix || ""} ${capturedAtLabel}`.trim() + : point.location || "" content.append(location) if (point.id) { @@ -1145,10 +1156,14 @@ export const Hooks = { const date = new Date(value) if (Number.isNaN(date.getTime())) return - this.el.textContent = new Intl.DateTimeFormat(undefined, { - dateStyle: "medium", - timeStyle: "short" - }).format(date) + const dateStyle = this.el.dataset.dateStyle || "medium" + const timeStyle = this.el.dataset.timeStyle || "short" + const options = {} + + if (dateStyle !== "none") options.dateStyle = dateStyle + if (timeStyle !== "none") options.timeStyle = timeStyle + + this.el.textContent = new Intl.DateTimeFormat(undefined, options).format(date) this.el.title = date.toISOString() } diff --git a/e2e/tests/notifications-data.spec.ts b/e2e/tests/notifications-data.spec.ts index 8852108..3f7bb72 100644 --- a/e2e/tests/notifications-data.spec.ts +++ b/e2e/tests/notifications-data.spec.ts @@ -35,6 +35,7 @@ test("nearby alerts, private notification inbox, preferences, and data export wo const requestTitle = projectText("Nearby notification request", scope); await gotoLiveView(subscriber.page, "/notifications"); + await subscriber.page.getByRole("link", { name: "Delivery settings" }).click(); const preferenceForm = subscriber.page.locator("#notification-preferences-form"); await preferenceForm.getByLabel("Allow email notifications").check(); await preferenceForm.getByLabel("Nearby requests by email").check(); @@ -44,6 +45,7 @@ test("nearby alerts, private notification inbox, preferences, and data export wo await preferenceForm.getByRole("button", { name: "Save preferences" }).click(); await expect(subscriber.page.getByText("Notification preferences saved.")).toBeVisible(); + await subscriber.page.getByRole("link", { name: "Nearby alerts" }).click(); const subscriptionForm = subscriber.page.locator("#nearby-subscription-form"); await subscriptionForm.getByLabel("Alert name").fill(alertName); await subscriptionForm.getByLabel("Private area label").fill(privateArea); @@ -96,6 +98,7 @@ test("nearby alerts, private notification inbox, preferences, and data export wo await expect(requester.page.getByRole("heading", { name: requestTitle })).toBeVisible(); const requestURL = requester.page.url(); + await subscriber.page.getByRole("link", { name: "Inbox" }).click(); const inbox = subscriber.page .getByRole("heading", { name: "Inbox" }) .locator("xpath=ancestor::section"); diff --git a/lib/who_need_help/help.ex b/lib/who_need_help/help.ex index c8c5643..1cf0d24 100644 --- a/lib/who_need_help/help.ex +++ b/lib/who_need_help/help.ex @@ -886,7 +886,8 @@ defmodule WhoNeedHelp.Help do request.id, assignment.helper_id, "Request cancelled", - "The requester cancelled this request. Open Who Need Help for details." + "The requester cancelled this request. Open Who Need Help for details.", + %{"event_variant" => "requester_cancelled"} ) end @@ -908,7 +909,8 @@ defmodule WhoNeedHelp.Help do request.id, request.requester_id, "Helper withdrew after expiry", - "The helper withdrew and the request is no longer open because it expired." + "The helper withdrew and the request is no longer open because it expired.", + %{"event_variant" => "helper_withdrew_after_expiry"} ) end diff --git a/lib/who_need_help/notifications/text.ex b/lib/who_need_help/notifications/text.ex new file mode 100644 index 0000000..0964579 --- /dev/null +++ b/lib/who_need_help/notifications/text.ex @@ -0,0 +1,153 @@ +defmodule WhoNeedHelp.Notifications.Text do + @moduledoc """ + Renders privacy-safe notification copy in the recipient's locale. + + Notification rows retain their original title and body as a compatibility + fallback. Known product events are rendered from `kind` and structured + `data`, so the inbox and remote delivery can follow a user's current locale + without storing private conversation text or coordinates. + """ + + use Gettext, backend: WhoNeedHelpWeb.Gettext + + alias WhoNeedHelp.Notifications.Notification + + @supported_locales ~w(en uk ru) + + def render(%Notification{} = notification, locale) do + with_locale(locale, fn -> + notification + |> content() + |> Map.put(:locale, normalized_locale(locale)) + end) + end + + def localize(%Notification{} = notification, locale) do + %{title: title, body: body} = render(notification, locale) + %{notification | title: title, body: body} + end + + defp content(%Notification{kind: :nearby_request}) do + %{ + title: gettext("New help request nearby"), + body: gettext("A request matching one of your nearby-help alerts is available.") + } + end + + defp content(%Notification{kind: :request_accepted}) do + %{ + title: gettext("A helper responded"), + body: gettext("Open Who Need Help to see the request update.") + } + end + + defp content(%Notification{kind: :request_reopened}) do + %{ + title: gettext("Your request needs a new helper"), + body: gettext("The previous helper withdrew, so the request is open again.") + } + end + + defp content(%Notification{ + kind: :request_cancelled, + data: %{"event_variant" => "requester_cancelled"} + }) do + %{ + title: gettext("Request cancelled"), + body: gettext("The requester cancelled this request. Open Who Need Help for details.") + } + end + + defp content(%Notification{ + kind: :request_cancelled, + data: %{"event_variant" => "helper_withdrew_after_expiry"} + }) do + %{ + title: gettext("Request closed after the helper withdrew"), + body: gettext("The helper withdrew after the request expired, so it was not reopened.") + } + end + + defp content(%Notification{kind: :request_cancelled}) do + %{ + title: gettext("Request updated"), + body: gettext("Open Who Need Help to review the latest request status.") + } + end + + defp content(%Notification{kind: :assignment_started}) do + %{ + title: gettext("Your helper started"), + body: gettext("Open Who Need Help to follow the request status.") + } + end + + defp content(%Notification{kind: :helper_arrived}) do + %{ + title: gettext("Your helper arrived"), + body: gettext("Open Who Need Help to coordinate the handover safely.") + } + end + + defp content(%Notification{kind: :handover_verified}) do + %{ + title: gettext("Handover code verified"), + body: gettext("The helper verified the one-time handover code.") + } + end + + defp content(%Notification{kind: :request_completed}) do + %{ + title: gettext("Help completed"), + body: gettext("Both participants confirmed completion and the handover was verified.") + } + end + + defp content(%Notification{kind: :message_created}) do + %{ + title: gettext("New message"), + body: gettext("Open Who Need Help to read the conversation.") + } + end + + defp content(%Notification{kind: :review_revealed}) do + %{ + title: gettext("Your review is now visible"), + body: gettext("Both participants submitted a review. Open Who Need Help to see it.") + } + end + + defp content(%Notification{ + kind: :support_update, + data: %{"kind" => "report_updated", "status" => status} + }) do + %{ + title: gettext("Your report was updated"), + body: gettext("The latest moderation status is %{status}.", status: status_label(status)) + } + end + + defp content(%Notification{kind: :support_update}) do + %{ + title: gettext("Support request updated"), + body: gettext("Open Who Need Help to review the latest update.") + } + end + + defp content(%Notification{} = notification) do + %{title: notification.title, body: notification.body} + end + + defp status_label("open"), do: gettext("Open") + defp status_label("reviewing"), do: gettext("Reviewing") + defp status_label("resolved"), do: gettext("Resolved") + defp status_label("dismissed"), do: gettext("Dismissed") + defp status_label(status), do: status + + defp with_locale(locale, fun) do + Gettext.with_locale(WhoNeedHelpWeb.Gettext, normalized_locale(locale), fun) + end + + defp normalized_locale(locale) when locale in @supported_locales, do: locale + defp normalized_locale(_locale), do: "en" +end diff --git a/lib/who_need_help/push.ex b/lib/who_need_help/push.ex index 79f6514..73fd6f9 100644 --- a/lib/who_need_help/push.ex +++ b/lib/who_need_help/push.ex @@ -66,7 +66,7 @@ defmodule WhoNeedHelp.Push do }) end - def enqueue_lifecycle(kind, event_id, request_id, recipient_id, title, body) + def enqueue_lifecycle(kind, event_id, request_id, recipient_id, title, body, data \\ %{}) when is_atom(kind) do Notifications.notify_user(recipient_id, %{ kind: kind, @@ -74,7 +74,11 @@ defmodule WhoNeedHelp.Push do body: body, path: "/requests/#{request_id}", idempotency_key: "#{kind}:#{event_id}:#{recipient_id}", - data: %{"kind" => to_string(kind), "request_id" => request_id} + data: + Map.merge( + %{"kind" => to_string(kind), "request_id" => request_id}, + Map.new(data, fn {key, value} -> {to_string(key), value} end) + ) }) end diff --git a/lib/who_need_help/push/device_delivery_worker.ex b/lib/who_need_help/push/device_delivery_worker.ex index c3157b7..0a24ef0 100644 --- a/lib/who_need_help/push/device_delivery_worker.ex +++ b/lib/who_need_help/push/device_delivery_worker.ex @@ -11,7 +11,7 @@ defmodule WhoNeedHelp.Push.DeviceDeliveryWorker do ] alias WhoNeedHelp.Notifications - alias WhoNeedHelp.Notifications.{Notification, PushDevice} + alias WhoNeedHelp.Notifications.{Notification, PushDevice, Text} alias WhoNeedHelp.Push alias WhoNeedHelp.Repo @@ -19,7 +19,14 @@ defmodule WhoNeedHelp.Push.DeviceDeliveryWorker do def perform(%Oban.Job{ args: %{"notification_id" => notification_id, "device_id" => device_id} }) do - notification = Repo.get(Notification, notification_id) + notification = + Notification + |> Repo.get(notification_id) + |> case do + nil -> nil + notification -> Repo.preload(notification, :user) + end + device = Repo.get(PushDevice, device_id) cond do @@ -33,7 +40,7 @@ defmodule WhoNeedHelp.Push.DeviceDeliveryWorker do {:cancel, :ownership_mismatch} true -> - deliver(notification, device) + deliver(Text.localize(notification, notification.user.locale), device) end end diff --git a/lib/who_need_help/push/notification_dispatch_worker.ex b/lib/who_need_help/push/notification_dispatch_worker.ex index 935b3a4..35e3ba4 100644 --- a/lib/who_need_help/push/notification_dispatch_worker.ex +++ b/lib/who_need_help/push/notification_dispatch_worker.ex @@ -6,7 +6,7 @@ defmodule WhoNeedHelp.Push.NotificationDispatchWorker do unique: [period: :infinity, fields: [:args, :worker], keys: [:notification_id]] alias WhoNeedHelp.Notifications - alias WhoNeedHelp.Notifications.{Notification, Preference} + alias WhoNeedHelp.Notifications.{Notification, Preference, Text} alias WhoNeedHelp.Push alias WhoNeedHelp.Push.{DeliveryWorker, DeviceDeliveryWorker, NotificationEmailWorker} alias WhoNeedHelp.Repo @@ -14,8 +14,10 @@ defmodule WhoNeedHelp.Push.NotificationDispatchWorker do @impl Oban.Worker def perform(%Oban.Job{args: %{"notification_id" => notification_id}} = job) do with %Notification{} = notification <- Repo.get(Notification, notification_id) do + notification = Repo.preload(notification, :user) if job.attempt == 1, do: Notifications.broadcast_created(notification) now = DateTime.utc_now(:second) + localized_notification = Text.localize(notification, notification.user.locale) preference = Repo.get_by(Preference, user_id: notification.user_id) || @@ -38,7 +40,7 @@ defmodule WhoNeedHelp.Push.NotificationDispatchWorker do {:snooze, seconds} true -> - case dispatch_push(notification, push_allowed?) do + case dispatch_push(localized_notification, push_allowed?) do :ok -> dispatch_email(notification, email_allowed?) {:error, _reason} = error -> error end diff --git a/lib/who_need_help/push/notification_email_worker.ex b/lib/who_need_help/push/notification_email_worker.ex index c76767d..7ca6e96 100644 --- a/lib/who_need_help/push/notification_email_worker.ex +++ b/lib/who_need_help/push/notification_email_worker.ex @@ -7,7 +7,7 @@ defmodule WhoNeedHelp.Push.NotificationEmailWorker do unique: [period: :infinity, fields: [:args, :worker], keys: [:notification_id]] alias WhoNeedHelp.Notifications - alias WhoNeedHelp.Notifications.{EmailNotifier, Notification, Preference} + alias WhoNeedHelp.Notifications.{EmailNotifier, Notification, Preference, Text} alias WhoNeedHelp.Repo @impl Oban.Worker @@ -26,7 +26,9 @@ defmodule WhoNeedHelp.Push.NotificationEmailWorker do %Preference{user_id: notification.user_id} if Notifications.email_allowed?(preference, notification) do - case EmailNotifier.deliver(notification.user, notification) do + localized_notification = Text.localize(notification, notification.user.locale) + + case EmailNotifier.deliver(notification.user, localized_notification) do {:ok, _metadata} -> :ok {:error, reason} -> {:error, reason} end diff --git a/lib/who_need_help/trust.ex b/lib/who_need_help/trust.ex index 065ea02..366df34 100644 --- a/lib/who_need_help/trust.ex +++ b/lib/who_need_help/trust.ex @@ -399,7 +399,11 @@ defmodule WhoNeedHelp.Trust do body: "The latest moderation status is #{report.status}.", path: "/reports", idempotency_key: "report-moderated:#{report.id}:#{report.status}", - data: %{"kind" => "report_updated", "report_id" => report.id} + data: %{ + "kind" => "report_updated", + "report_id" => report.id, + "status" => to_string(report.status) + } }) result diff --git a/lib/who_need_help_web/components/core_components.ex b/lib/who_need_help_web/components/core_components.ex index a0da1dc..0084db2 100644 --- a/lib/who_need_help_web/components/core_components.ex +++ b/lib/who_need_help_web/components/core_components.ex @@ -472,6 +472,39 @@ defmodule WhoNeedHelpWeb.CoreComponents do """ end + @doc """ + Renders a timestamp in the browser's locale and time zone. + + UTC remains the durable server value and the no-JavaScript fallback. The + `LocalTime` hook replaces only the visible text, so the semantic `time` + element keeps its machine-readable ISO timestamp. + """ + attr :id, :string, required: true + attr :value, :any, required: true + attr :class, :string, default: nil + attr :date_style, :string, values: ~w(full long medium short none), default: "medium" + attr :time_style, :string, values: ~w(full long medium short none), default: "short" + + def local_time(assigns) do + assigns = + assigns + |> assign(:iso_value, DateTime.to_iso8601(assigns.value)) + |> assign(:fallback, Calendar.strftime(assigns.value, "%Y-%m-%d %H:%M UTC")) + + ~H""" + + """ + end + defp unused_input_name(name) do case Regex.run(~r/^(.*)\[([^\[\]]+)\]$/, name, capture: :all_but_first) do [prefix, field] -> "#{prefix}[_unused_#{field}]" diff --git a/lib/who_need_help_web/controllers/support_html.ex b/lib/who_need_help_web/controllers/support_html.ex index 100b017..5cbc6d3 100644 --- a/lib/who_need_help_web/controllers/support_html.ex +++ b/lib/who_need_help_web/controllers/support_html.ex @@ -28,10 +28,4 @@ defmodule WhoNeedHelpWeb.SupportHTML do def event_actor_label(:staff), do: gettext("Support team") def event_actor_label(:system), do: gettext("System") def event_actor_label(value), do: to_string(value) - - def format_timestamp(%DateTime{} = value) do - Calendar.strftime(value, "%Y-%m-%d %H:%M UTC") - end - - def format_timestamp(value), do: to_string(value) end diff --git a/lib/who_need_help_web/controllers/support_html/show.html.heex b/lib/who_need_help_web/controllers/support_html/show.html.heex index 3887460..82e4124 100644 --- a/lib/who_need_help_web/controllers/support_html/show.html.heex +++ b/lib/who_need_help_web/controllers/support_html/show.html.heex @@ -13,7 +13,12 @@

{gettext("Your original request")}

{@request.details}

@@ -43,7 +48,12 @@
{message_sender_label(message.sender_role)}

{message.body}

@@ -126,7 +136,13 @@ >

{status_label(event.to_status)}

- {event_actor_label(event.actor_role)} · {format_timestamp(event.inserted_at)} + {event_actor_label(event.actor_role)} · + <.local_time + id={"support-event-created-#{event.id}"} + value={event.inserted_at} + date_style="short" + time_style="short" + />

  • 📍 {activity.location_label} - 🕒 {Calendar.strftime(activity.starts_at, "%d.%m.%Y, %H:%M UTC")} + + <.icon name="hero-clock" class="size-4" /> + <.local_time + id={"activity-start-#{activity.id}"} + value={activity.starts_at} + date_style="medium" + time_style="short" + /> + {gettext("by %{name}", name: activity.creator.display_name || gettext("Community member") diff --git a/lib/who_need_help_web/live/activity_live/show.ex b/lib/who_need_help_web/live/activity_live/show.ex index da97f55..e970294 100644 --- a/lib/who_need_help_web/live/activity_live/show.ex +++ b/lib/who_need_help_web/live/activity_live/show.ex @@ -377,13 +377,19 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do
    {gettext("Starts")}
    - {Calendar.strftime(@activity.starts_at, "%d.%m.%Y, %H:%M UTC")} + <.local_time + id={"activity-start-#{@activity.id}"} + value={@activity.starts_at} + />
    {gettext("Join requests close")}
    - {Calendar.strftime(@activity.join_deadline, "%d.%m.%Y, %H:%M UTC")} + <.local_time + id={"activity-join-deadline-#{@activity.id}"} + value={@activity.join_deadline} + />
    @@ -475,14 +481,11 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do {message.sender.display_name || gettext("Participant")}
    - + />
    -
    + + +

    {gettext("Inbox")}

    @@ -260,13 +306,18 @@ defmodule WhoNeedHelpWeb.NotificationLive do notification.read_at && "bg-transparent" ]} /> - {notification.title} + <% text = notification_text(notification) %> + {text.title} - {notification.body} - - - {format_time(notification.inserted_at)} + {text.body} + <.local_time + id={"notification-time-#{notification.id}"} + value={notification.inserted_at} + date_style="short" + time_style="short" + class="mt-2 block text-xs font-normal text-base-content/45" + /> <.icon name="hero-chevron-right" class="mt-2 size-4 shrink-0 opacity-45" /> @@ -282,12 +333,12 @@ defmodule WhoNeedHelpWeb.NotificationLive do
    -
    +

    {gettext("Delivery channels and quiet hours")}

    @@ -328,7 +379,13 @@ defmodule WhoNeedHelpWeb.NotificationLive do

    {device.device_label || gettext("Web browser")}

    -

    {format_time(device.last_seen_at)}

    + <.local_time + id={"push-device-last-seen-#{device.id}"} + value={device.last_seen_at} + date_style="short" + time_style="short" + class="text-xs text-base-content/55" + />
    -
    -

    {gettext("Your nearby alerts")}

    -

    - {gettext("The saved center is private and is used only to find matching requests.")} -

    +
    +

    {gettext("Your nearby alerts")}

    +

    + {gettext("The saved center is private and is used only to find matching requests.")} +

    -
    - {gettext("No nearby alerts configured yet.")} -
    +
    + {gettext("No nearby alerts configured yet.")} +
    -
    -
    -
    -
    -

    {subscription.name}

    -

    - {subscription.location_label} · {div(subscription.radius_meters, 1_000)} km -

    -
    - - {gettext("Push")} - - - {gettext("Email")} - -
    +
    +
    +
    +
    +

    {subscription.name}

    +

    + {subscription.location_label} · {div(subscription.radius_meters, 1_000)} km +

    +
    + + {gettext("Push")} + + + {gettext("Email")} +
    - - {if subscription.active, do: gettext("Active"), else: gettext("Paused")} - -
    -
    - -
    + + {if subscription.active, do: gettext("Active"), else: gettext("Paused")} + +
    +
    + +
    -
    +

    {gettext("Create a nearby alert")}

    {gettext("Choose an area, categories, urgency, and the times when you are available.")} diff --git a/lib/who_need_help_web/live/report_live.ex b/lib/who_need_help_web/live/report_live.ex index 0f14d01..b1002a5 100644 --- a/lib/who_need_help_web/live/report_live.ex +++ b/lib/who_need_help_web/live/report_live.ex @@ -69,9 +69,13 @@ defmodule WhoNeedHelpWeb.ReportLive do

    {gettext("Moderation response")}

    {report.resolution_note}

    - + <.local_time + id={"report-created-#{report.id}"} + value={report.inserted_at} + date_style="short" + time_style="short" + class="mt-4 block text-xs text-base-content/55" + />
    - - {gettext("expires %{time}", - time: Calendar.strftime(request.expires_at, "%d.%m.%Y, %H:%M UTC") - )} + + {gettext("Expires")} + <.local_time + id={"request-expiry-#{request.id}"} + value={request.expires_at} + date_style="short" + time_style="short" + />
    diff --git a/lib/who_need_help_web/live/request_live/show.ex b/lib/who_need_help_web/live/request_live/show.ex index c064636..e7eaafb 100644 --- a/lib/who_need_help_web/live/request_live/show.ex +++ b/lib/who_need_help_web/live/request_live/show.ex @@ -905,11 +905,8 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do exact: true, type: "live", title: live_location_title(user_id, request, assignment), - location: - gettext( - "Last update: %{time}", - time: Calendar.strftime(point.captured_at, "%d.%m.%Y, %H:%M:%S UTC") - ) + location_prefix: gettext("Last update:"), + captured_at: DateTime.to_iso8601(point.captured_at) } end) end @@ -1073,10 +1070,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do
    {gettext("Expires:")} - {Calendar.strftime( - @request.expires_at, - "%d.%m.%Y, %H:%M UTC" - )} + <.local_time id={"request-expiry-#{@request.id}"} value={@request.expires_at} />
    {gettext("Location:")} @@ -1168,13 +1162,12 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do >
    {message.sender.display_name} - + value={message.inserted_at} + date_style="short" + time_style="short" + />
    {message.body}
    "helper_withdrew_after_expiry" + }) + + assert Text.render(notification, "ru").title == "Заявка закрыта после отказа помощника" + + assert Text.render(notification, "uk").body == + "Помічник відмовився після завершення строку запиту, тому його не відкрито повторно." + end + + test "localizes structured moderation status and falls back to English locale" do + notification = + notification(:support_update, %{ + "kind" => "report_updated", + "status" => "reviewing" + }) + + assert Text.render(notification, "ru").body == "Текущий статус модерации: На рассмотрении." + assert Text.render(notification, "uk").body == "Поточний статус модерації: На розгляді." + assert Text.render(notification, "unsupported").locale == "en" + end + + defp notification(kind, data \\ %{}) do + %Notification{ + kind: kind, + title: "Stored fallback title", + body: "Stored fallback body", + data: data + } + end +end diff --git a/test/who_need_help_web/live/mutual_aid_live_test.exs b/test/who_need_help_web/live/mutual_aid_live_test.exs index 7ec6663..55db211 100644 --- a/test/who_need_help_web/live/mutual_aid_live_test.exs +++ b/test/who_need_help_web/live/mutual_aid_live_test.exs @@ -82,15 +82,22 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do assert html =~ "Notifications" assert html =~ "Support request updated" assert html =~ "1 unread" - assert html =~ ~s(data-enable-label="Enable push on this device") - assert html =~ ~s(data-enabled-label="Push enabled on this device") - assert has_element?(view, "[data-disable-device='#{push_device.id}']") view |> element("button[phx-click='mark-all-read']") |> render_click() assert render(view) =~ "0 unread" assert Notifications.unread_count(scope) == 0 assert Repo.get!(WhoNeedHelp.Notifications.Notification, notification.id).read_at + view + |> element("a[href='/notifications?section=settings']") + |> render_click() + + assert_patch(view, ~p"/notifications?section=settings") + html = render(view) + assert html =~ ~s(data-enable-label="Enable push on this device") + assert html =~ ~s(data-enabled-label="Push enabled on this device") + assert has_element?(view, "[data-disable-device='#{push_device.id}']") + view |> form("#notification-preferences-form", %{ "notification_preference" => %{ @@ -111,6 +118,12 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do assert render(view) =~ "Notification preferences saved." + view + |> element("a[href='/notifications?section=alerts']") + |> render_click() + + assert_patch(view, ~p"/notifications?section=alerts") + view |> form("#nearby-subscription-form", %{ "nearby_subscription" => %{