fix(notifications): a notice value is no plain string, so a translated name cannot freeze in one language

A name goes in as raw(), a translated word as its own msg(): tsc now lists every value, and the
ones that froze are said when shown — Watch later and an untitled channel in the pin and channel
notices, a Plex season's name in its removal toast (a new seasonTitleText, which seasonTitle says).
This commit is contained in:
2026-10-30 00:03:48 +01:00
parent cb8b99e0e7
commit 4a0d4b0964
37 changed files with 174 additions and 109 deletions
@@ -11,7 +11,7 @@ import { hydrateSort, sortRows, type SortState } from "../../lib/columnSort";
import { usePref } from "../../lib/usePref";
import { useCardPager } from "../../lib/useCardPager";
import { subsColumn } from "./channelColumns";
import { msg, notify } from "../../lib/notifications";
import { msg, notify, raw } from "../../lib/notifications";
import { notifyYouTubeActionError } from "../../lib/youtubeErrors";
import Tooltip from "../../components/ui/Tooltip";
import ChannelLink from "./ChannelLink";
@@ -71,7 +71,7 @@ export default function ChannelDiscovery({
notify({
level: "success",
title: msg("channels.discovery.subscribedTitle"),
message: msg("channels.discovery.subscribedBody", { name }),
message: msg("channels.discovery.subscribedBody", { name: raw(name) }),
meta: { kind: "channel-subscribed", channelId: c.id, channelName: name },
});
},
+6 -3
View File
@@ -18,7 +18,7 @@ import { Sparkles } from "lucide-react";
import { notifyYouTubeActionError } from "../../lib/youtubeErrors";
import { LS, ownKey } from "../../lib/storage";
import { buildChannelColumns, applySyncFilter, tagFilterChips } from "./channelColumns";
import { msg, notify } from "../../lib/notifications";
import { msg, notify, raw } from "../../lib/notifications";
import { Badge } from "../../components/ui/Badge";
import { Button } from "../../components/ui/Button";
import Tooltip from "../../components/ui/Tooltip";
@@ -168,7 +168,7 @@ export default function Channels() {
(tagId: number, name: string) => {
setFeedFilters({ ...feedFilters, tags: [tagId], channelIds: [] });
setPage("feed");
notify({ level: "info", message: msg("channels.notify.filteredByTag", { name }) });
notify({ level: "info", message: msg("channels.notify.filteredByTag", { name: raw(name) }) });
},
[setFeedFilters, feedFilters, setPage],
);
@@ -406,7 +406,10 @@ export default function Channels() {
((channelsQuery.data ?? []).find((c) => c.id === rowMenu.target!.id) ?? rowMenu.target);
const menuItems = menuRow
? channelActions.items(menuRow, {
pin: rowPin.item(pinChannel(menuRow.id), menuRow.title ?? t("channels.row.thisChannel")),
pin: rowPin.item(
pinChannel(menuRow.id),
menuRow.title == null ? msg("channels.row.thisChannel") : raw(menuRow.title),
),
})
: [];
@@ -33,7 +33,7 @@ vi.mock("../../lib/useMe", () => ({ useMe: () => ({ role: h.role, can_write: h.c
import { useChannelActions } from "./useChannelActions";
import { api, type ManagedChannel, type Tag } from "../../lib/api";
import type { ActionMenuItem } from "../../components/ui/ActionMenu";
import { msg, type NotifText } from "../../lib/notifications";
import { msg, type NotifText, raw } from "../../lib/notifications";
afterEach(() => {
cleanup();
@@ -199,11 +199,24 @@ describe("useChannelActions — hide and unsubscribe", () => {
message: NotifText;
action: { onClick: () => void };
};
expect(toast.message).toEqual(msg("channels.notify.hidden", { name: "Chan" }));
expect(toast.message).toEqual(msg("channels.notify.hidden", { name: raw("Chan") }));
act(() => toast.action.onClick());
await waitFor(() => expect(upd).toHaveBeenCalledWith("UC1", { hidden: false }));
});
it('names a channel with no title yet as "this channel" in the reader\'s language when shown', async () => {
vi.spyOn(api, "updateChannel").mockResolvedValue({} as never);
const { result } = mount();
act(() =>
result.current.items(ch({ title: null }), { pin }).find((i) => i.id === "hide")!.onSelect!(),
);
await waitFor(() => expect(h.notify).toHaveBeenCalled());
const toast = h.notify.mock.calls[0]![0] as { message: NotifText };
expect(toast.message).toEqual(
msg("channels.notify.hidden", { name: msg("channels.row.thisChannel") }),
);
});
it("unsubscribes only after the danger confirm says yes", async () => {
const unsub = vi.spyOn(api, "unsubscribeChannel").mockResolvedValue({} as never);
const { result } = mount();
@@ -7,7 +7,7 @@ import { api, type ManagedChannel, type Tag } from "../../lib/api";
import { priorityChoices } from "../../lib/channelOrder";
import { compareText } from "../../lib/collate";
import { channelYouTubeUrl } from "../../lib/format";
import { msg, notify } from "../../lib/notifications";
import { msg, notify, raw } from "../../lib/notifications";
import { notifyYouTubeActionError } from "../../lib/youtubeErrors";
import { useMe } from "../../lib/useMe";
import { useConfirm } from "../../components/ui/confirmContext";
@@ -46,7 +46,9 @@ export function useChannelActions({
const me = useMe();
const isAdmin = me.role === "admin";
const copy = useCopyItems();
const nameOf = (c: ManagedChannel) => c.title ?? t("channels.row.thisChannel");
// The notice says it when shown: a channel with no title yet is "this channel" in the reader's language.
const nameOf = (c: ManagedChannel) =>
c.title == null ? msg("channels.row.thisChannel") : raw(c.title);
const refresh = () => {
qc.invalidateQueries({ queryKey: qk.channels() });
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { qk } from "../../lib/queryKeys";
import { invalidateSubscription } from "../../lib/subscriptionCache";
import { api, type WatchedChannel } from "../../lib/api";
import { msg, notify } from "../../lib/notifications";
import { msg, notify, raw } from "../../lib/notifications";
import { notifyYouTubeActionError } from "../../lib/youtubeErrors";
import { useConfirm } from "../../components/ui/confirmContext";
@@ -22,7 +22,7 @@ export function useWatchedChannelActions(onOpenWizard: () => void) {
notify({
level: "success",
title: msg("channels.discovery.subscribedTitle"),
message: msg("channels.discovery.subscribedBody", { name: nameOf(c) }),
message: msg("channels.discovery.subscribedBody", { name: raw(nameOf(c)) }),
meta: { kind: "channel-subscribed", channelId: c.id, channelName: nameOf(c) },
});
},
@@ -43,7 +43,7 @@ export function useWatchedChannelActions(onOpenWizard: () => void) {
void qc.invalidateQueries({ queryKey: qk.watchedChannels() });
notify({
level: "success",
message: msg("channels.watched.dismissed", { name: nameOf(c) }),
message: msg("channels.watched.dismissed", { name: raw(nameOf(c)) }),
action: { label: t("common.undo"), onClick: () => restore.mutate(c) },
});
},
@@ -54,7 +54,7 @@ export function useWatchedChannelActions(onOpenWizard: () => void) {
const onSubscribe = async (c: WatchedChannel): Promise<boolean> => {
const ok = await confirm({
title: t("channels.discovery.subscribe"),
message: t("channels.discovery.confirmSubscribe", { name: nameOf(c) }),
message: t("channels.discovery.confirmSubscribe", { name: raw(nameOf(c)) }),
confirmLabel: t("channels.discovery.subscribe"),
});
if (ok) subscribe.mutate(c);