v4: every message named by an id carries its English in the code — the emails and the four pages that share their words called Lingui with a bare id (i18n._("digest.community"), <Trans id="digest.community" />), so their English lived only in a hand-kept en.po, extract couldn't run with --overwrite, and a page reading the code couldn't tell what it said. The emails now pass descriptors, i18n._({ id, message, values }), the form Lingui reads without a macro, as the server needs. The fifteen messages the digest shares with the community page, Settings, the unsubscribe page and the avatar menu are declared once, as /*i18n*/ descriptors in shared/digest-messages.ts, which those pages translate with useLingui()'s i18n instead of the global one. lingui.config.ts scans shared/ too, and i18n:extract runs with --overwrite, so en.po is written from the code. Every other message already used the macros. The compiled catalogs are unchanged, key for key, in both languages. 272 backend tests and 677 frontend tests pass, and deno check, tsc, eslint, doc:lint and i18n:compile --strict are clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
import { EMAIL_LOCALE, OG_SITE_NAME } from "../config.ts";
|
||||
import type { DigestCadence } from "../../shared/settings.ts";
|
||||
import { i18n } from "./i18n.ts";
|
||||
import { DIGEST_MESSAGES } from "../../shared/digest-messages.ts";
|
||||
import {
|
||||
EmailKit,
|
||||
type EmailLook,
|
||||
@@ -127,17 +128,33 @@ const ACTIVITY_ORDER: ActivityKind[] = [
|
||||
function countLabel(kind: ActivityKind, n: number): string {
|
||||
switch (kind) {
|
||||
case "upvotes":
|
||||
return i18n._("digest.count.upvotes", { n });
|
||||
return i18n._({ ...DIGEST_MESSAGES.countUpvotes, values: { n } });
|
||||
case "followers":
|
||||
return i18n._("email.digest.count.followers", { n });
|
||||
return i18n._({
|
||||
id: "email.digest.count.followers",
|
||||
message: "{n, plural, one {new follower} other {new followers}}",
|
||||
values: { n },
|
||||
});
|
||||
case "comments":
|
||||
return i18n._("digest.count.comments", { n });
|
||||
return i18n._({ ...DIGEST_MESSAGES.countComments, values: { n } });
|
||||
case "mentions":
|
||||
return i18n._("email.digest.count.mentions", { n });
|
||||
return i18n._({
|
||||
id: "email.digest.count.mentions",
|
||||
message: "{n, plural, one {mention} other {mentions}}",
|
||||
values: { n },
|
||||
});
|
||||
case "likes":
|
||||
return i18n._("email.digest.count.likes", { n });
|
||||
return i18n._({
|
||||
id: "email.digest.count.likes",
|
||||
message: "{n, plural, one {comment like} other {comment likes}}",
|
||||
values: { n },
|
||||
});
|
||||
case "playlistFollows":
|
||||
return i18n._("email.digest.count.playlistFollows", { n });
|
||||
return i18n._({
|
||||
id: "email.digest.count.playlistFollows",
|
||||
message: "{n, plural, one {playlist follow} other {playlist follows}}",
|
||||
values: { n },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,36 +170,87 @@ function highlightSentence(
|
||||
const values = { who, target };
|
||||
switch (kind) {
|
||||
case "mentionComment":
|
||||
return i18n._("email.digest.hl.mentionComment", values);
|
||||
return i18n._({
|
||||
id: "email.digest.hl.mentionComment",
|
||||
message: "{who} mentioned you in a comment on {target}",
|
||||
values,
|
||||
});
|
||||
case "mentionDump":
|
||||
case "mentionChat":
|
||||
return i18n._("email.digest.hl.mentionIn", values);
|
||||
return i18n._({
|
||||
id: "email.digest.hl.mentionIn",
|
||||
message: "{who} mentioned you in {target}",
|
||||
values,
|
||||
});
|
||||
case "mentionPlaylist":
|
||||
return i18n._("email.digest.hl.mentionPlaylist", values);
|
||||
return i18n._({
|
||||
id: "email.digest.hl.mentionPlaylist",
|
||||
message: "{who} mentioned you in the playlist {target}",
|
||||
values,
|
||||
});
|
||||
case "mentionEveryone":
|
||||
return i18n._("email.digest.hl.mentionEveryone", values);
|
||||
return i18n._({
|
||||
id: "email.digest.hl.mentionEveryone",
|
||||
message: "{who} mentioned everyone in {target}",
|
||||
values,
|
||||
});
|
||||
case "comment":
|
||||
return i18n._("email.digest.hl.comment", values);
|
||||
return i18n._({
|
||||
id: "email.digest.hl.comment",
|
||||
message: "{who} commented on your dump {target}",
|
||||
values,
|
||||
});
|
||||
case "playlistFollow":
|
||||
return i18n._("email.digest.hl.playlistFollow", values);
|
||||
return i18n._({
|
||||
id: "email.digest.hl.playlistFollow",
|
||||
message: "{who} followed your playlist {target}",
|
||||
values,
|
||||
});
|
||||
case "userFollow":
|
||||
return i18n._("email.digest.hl.userFollow", values);
|
||||
return i18n._({
|
||||
id: "email.digest.hl.userFollow",
|
||||
message: "{who} started following you",
|
||||
values,
|
||||
});
|
||||
case "commentLike":
|
||||
return i18n._("email.digest.hl.commentLike", values);
|
||||
return i18n._({
|
||||
id: "email.digest.hl.commentLike",
|
||||
message: "{who} liked your comment on {target}",
|
||||
values,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const byLine = (username: string) => i18n._("email.digest.by", { username });
|
||||
const byLine = (username: string) =>
|
||||
i18n._({
|
||||
id: "email.digest.by",
|
||||
message: "by @{username}",
|
||||
values: { username },
|
||||
});
|
||||
const addedToLine = (playlist: string) =>
|
||||
i18n._("email.digest.addedTo", { playlist });
|
||||
const uploadLabel = () => i18n._("email.digest.upload");
|
||||
i18n._({
|
||||
id: "email.digest.addedTo",
|
||||
message: "added to {playlist}",
|
||||
values: { playlist },
|
||||
});
|
||||
const uploadLabel = () =>
|
||||
i18n._({ id: "email.digest.upload", message: "upload" });
|
||||
const invitedLine = (inviter: string, reader: string) =>
|
||||
inviter === reader
|
||||
? i18n._("digest.invitedByYou")
|
||||
: i18n._("digest.invitedBy", { username: inviter });
|
||||
const andMoreLine = (n: number) => i18n._("email.digest.andMore", { n });
|
||||
? i18n._(DIGEST_MESSAGES.invitedByYou)
|
||||
: i18n._({ ...DIGEST_MESSAGES.invitedBy, values: { username: inviter } });
|
||||
const andMoreLine = (n: number) =>
|
||||
i18n._({
|
||||
id: "email.digest.andMore",
|
||||
message: "and {n} more",
|
||||
values: { n },
|
||||
});
|
||||
const playlistMeta = (owner: string, n: number) =>
|
||||
i18n._("email.digest.playlistMeta", { username: owner, n });
|
||||
i18n._({
|
||||
id: "email.digest.playlistMeta",
|
||||
message: "by @{username} · {n, plural, one {# dump} other {# dumps}}",
|
||||
values: { username: owner, n },
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the digest `content` describes, written as Markdown and drawn in
|
||||
@@ -205,7 +273,7 @@ export function renderDigestEmail(
|
||||
new Intl.DateTimeFormat(EMAIL_LOCALE, { weekday: "short" }).format(d);
|
||||
const figure = (n: number, label: string) =>
|
||||
`**${nf.format(n)}** ${escMd(label)}`;
|
||||
const yours = i18n._("email.digest.yours");
|
||||
const yours = i18n._({ id: "email.digest.yours", message: "yours" });
|
||||
/** A dump as a list item: its thumbnail, title, pill and detail. */
|
||||
const dumpItem = (d: DigestDump, bullet: string, image = true) =>
|
||||
`${bullet} ${image && d.thumbnail ? ` ` : ""}${
|
||||
@@ -223,16 +291,26 @@ export function renderDigestEmail(
|
||||
const md: string[] = [
|
||||
`*${
|
||||
escMd(
|
||||
`${monthly ? i18n._("digest.monthly") : i18n._("digest.weekly")} · ${
|
||||
range(true)
|
||||
}`,
|
||||
`${
|
||||
monthly
|
||||
? i18n._(DIGEST_MESSAGES.monthly)
|
||||
: i18n._(DIGEST_MESSAGES.weekly)
|
||||
} · ${range(true)}`,
|
||||
)
|
||||
}*`,
|
||||
`# ${
|
||||
escMd(
|
||||
monthly
|
||||
? i18n._("email.digest.monthly.title", { site: OG_SITE_NAME })
|
||||
: i18n._("email.digest.weekly.title", { username: content.reader }),
|
||||
? i18n._({
|
||||
id: "email.digest.monthly.title",
|
||||
message: "The month on {site}",
|
||||
values: { site: OG_SITE_NAME },
|
||||
})
|
||||
: i18n._({
|
||||
id: "email.digest.weekly.title",
|
||||
message: "Your week, {username}",
|
||||
values: { username: content.reader },
|
||||
}),
|
||||
)
|
||||
}`,
|
||||
];
|
||||
@@ -240,7 +318,7 @@ export function renderDigestEmail(
|
||||
const { activity, network, community } = content;
|
||||
if (activity) {
|
||||
md.push(
|
||||
`## ${escMd(i18n._("email.digest.forYou"))}`,
|
||||
`## ${escMd(i18n._({ id: "email.digest.forYou", message: "For you" }))}`,
|
||||
ACTIVITY_ORDER.filter((k) => (activity.counts[k] ?? 0) > 0)
|
||||
.map((k) =>
|
||||
figure(activity.counts[k]!, countLabel(k, activity.counts[k]!))
|
||||
@@ -249,7 +327,7 @@ export function renderDigestEmail(
|
||||
activity.highlights.map((h) => {
|
||||
const target = h.kind === "mentionChat" || h.kind === "mentionEveryone"
|
||||
? mdLink(
|
||||
i18n._("email.digest.theChat"),
|
||||
i18n._({ id: "email.digest.theChat", message: "the chat" }),
|
||||
content.links.chat,
|
||||
)
|
||||
: h.target?.url
|
||||
@@ -264,7 +342,12 @@ export function renderDigestEmail(
|
||||
}).join("\n"),
|
||||
`${
|
||||
mdLink(
|
||||
i18n._("email.digest.seeAll", { n: activity.total }),
|
||||
i18n._({
|
||||
id: "email.digest.seeAll",
|
||||
message:
|
||||
"{n, plural, one {See the notification} other {See all # notifications}}",
|
||||
values: { n: activity.total },
|
||||
}),
|
||||
content.links.notifications,
|
||||
)
|
||||
} →`,
|
||||
@@ -273,13 +356,24 @@ export function renderDigestEmail(
|
||||
|
||||
if (network) {
|
||||
md.push(
|
||||
`## ${escMd(i18n._("email.digest.network"))}`,
|
||||
`## ${
|
||||
escMd(
|
||||
i18n._({
|
||||
id: "email.digest.network",
|
||||
message: "From people you follow",
|
||||
}),
|
||||
)
|
||||
}`,
|
||||
network.dumps.map((d) => dumpItem(d, "-")).join("\n"),
|
||||
);
|
||||
if (network.more > 0) {
|
||||
md.push(`${
|
||||
mdLink(
|
||||
i18n._("email.digest.more", { n: network.more }),
|
||||
i18n._({
|
||||
id: "email.digest.more",
|
||||
message: "{n} more on your Followed feed",
|
||||
values: { n: network.more },
|
||||
}),
|
||||
content.links.followed,
|
||||
)
|
||||
} →`);
|
||||
@@ -291,7 +385,9 @@ export function renderDigestEmail(
|
||||
md.push(
|
||||
`## ${
|
||||
escMd(
|
||||
monthly ? i18n._("digest.topMonth") : i18n._("digest.topWeek"),
|
||||
monthly
|
||||
? i18n._(DIGEST_MESSAGES.topMonth)
|
||||
: i18n._(DIGEST_MESSAGES.topWeek),
|
||||
)
|
||||
}`,
|
||||
);
|
||||
@@ -307,20 +403,23 @@ export function renderDigestEmail(
|
||||
if (community) {
|
||||
const { dumps, upvotes, comments } = community.totals;
|
||||
md.push(
|
||||
`## ${escMd(i18n._("digest.community"))}`,
|
||||
`## ${escMd(i18n._(DIGEST_MESSAGES.community))}`,
|
||||
);
|
||||
if (monthly) {
|
||||
md.push(
|
||||
[
|
||||
figure(
|
||||
dumps,
|
||||
i18n._("digest.stat.dumps", { n: dumps }),
|
||||
i18n._({ ...DIGEST_MESSAGES.statDumps, values: { n: dumps } }),
|
||||
),
|
||||
figure(upvotes, countLabel("upvotes", upvotes)),
|
||||
figure(comments, countLabel("comments", comments)),
|
||||
figure(
|
||||
community.chatMessages,
|
||||
i18n._("digest.stat.chat", { n: community.chatMessages }),
|
||||
i18n._({
|
||||
...DIGEST_MESSAGES.statChat,
|
||||
values: { n: community.chatMessages },
|
||||
}),
|
||||
),
|
||||
].join(" · "),
|
||||
);
|
||||
@@ -329,7 +428,7 @@ export function renderDigestEmail(
|
||||
md.push(
|
||||
`### ${
|
||||
escMd(
|
||||
i18n._("digest.newMembers"),
|
||||
i18n._(DIGEST_MESSAGES.newMembers),
|
||||
)
|
||||
}`,
|
||||
[
|
||||
@@ -348,7 +447,7 @@ export function renderDigestEmail(
|
||||
}
|
||||
if (community.playlists.length > 0) {
|
||||
md.push(
|
||||
`### ${escMd(i18n._("digest.newPlaylists"))}`,
|
||||
`### ${escMd(i18n._(DIGEST_MESSAGES.newPlaylists))}`,
|
||||
[
|
||||
...community.playlists.map((p) =>
|
||||
`- ${mdLink(p.title, p.url)} · ${
|
||||
@@ -363,12 +462,21 @@ export function renderDigestEmail(
|
||||
}
|
||||
if (!monthly && community.chatMessages > 0) {
|
||||
md.push(
|
||||
`### ${escMd(i18n._("email.digest.inChat"))}`,
|
||||
`### ${
|
||||
escMd(i18n._({ id: "email.digest.inChat", message: "In the chat" }))
|
||||
}`,
|
||||
`${
|
||||
escMd(i18n._("email.digest.chatWeek", { n: community.chatMessages }))
|
||||
escMd(
|
||||
i18n._({
|
||||
id: "email.digest.chatWeek",
|
||||
message:
|
||||
"{n, plural, one {# message this week} other {# messages this week}}",
|
||||
values: { n: community.chatMessages },
|
||||
}),
|
||||
)
|
||||
} · ${
|
||||
mdLink(
|
||||
i18n._("email.digest.openChat"),
|
||||
i18n._({ id: "email.digest.openChat", message: "Open the chat" }),
|
||||
content.links.chat,
|
||||
)
|
||||
}`,
|
||||
@@ -377,13 +485,21 @@ export function renderDigestEmail(
|
||||
if (!monthly && dumps + upvotes + comments > 0) {
|
||||
md.push(
|
||||
`*${
|
||||
escMd(i18n._("email.digest.totals", { dumps, upvotes, comments }))
|
||||
escMd(i18n._({
|
||||
id: "email.digest.totals",
|
||||
message:
|
||||
"{dumps, plural, one {# dump} other {# dumps}}, {upvotes, plural, one {# upvote} other {# upvotes}} and {comments, plural, one {# comment} other {# comments}}.",
|
||||
values: { dumps, upvotes, comments },
|
||||
}))
|
||||
}*`,
|
||||
);
|
||||
}
|
||||
md.push(`${
|
||||
mdLink(
|
||||
i18n._("email.digest.communityPage"),
|
||||
i18n._({
|
||||
id: "email.digest.communityPage",
|
||||
message: "See it all on the community page",
|
||||
}),
|
||||
content.links.community,
|
||||
)
|
||||
} →`);
|
||||
@@ -393,17 +509,27 @@ export function renderDigestEmail(
|
||||
"---",
|
||||
escMd(
|
||||
monthly
|
||||
? i18n._("email.digest.monthly.reason")
|
||||
: i18n._("email.digest.weekly.reason"),
|
||||
? i18n._({
|
||||
id: "email.digest.monthly.reason",
|
||||
message:
|
||||
"You're getting the monthly recap because it's on in your settings.",
|
||||
})
|
||||
: i18n._({
|
||||
id: "email.digest.weekly.reason",
|
||||
message:
|
||||
"You're getting the weekly digest because it's on in your settings.",
|
||||
}),
|
||||
),
|
||||
`${
|
||||
mdLink(
|
||||
monthly ? i18n._("digest.monthly.off") : i18n._("digest.weekly.off"),
|
||||
monthly
|
||||
? i18n._(DIGEST_MESSAGES.monthlyOff)
|
||||
: i18n._(DIGEST_MESSAGES.weeklyOff),
|
||||
content.links.unsubscribe,
|
||||
)
|
||||
} · ${
|
||||
mdLink(
|
||||
i18n._("email.settings"),
|
||||
i18n._({ id: "email.settings", message: "Settings" }),
|
||||
content.links.settings,
|
||||
)
|
||||
}`,
|
||||
@@ -411,13 +537,15 @@ export function renderDigestEmail(
|
||||
|
||||
return new EmailKit(look).render({
|
||||
subject: monthly
|
||||
? i18n._("email.digest.monthly.subject", {
|
||||
site: OG_SITE_NAME,
|
||||
range: range(false),
|
||||
? i18n._({
|
||||
id: "email.digest.monthly.subject",
|
||||
message: "The month on {site}, {range}",
|
||||
values: { site: OG_SITE_NAME, range: range(false) },
|
||||
})
|
||||
: i18n._("email.digest.weekly.subject", {
|
||||
site: OG_SITE_NAME,
|
||||
range: range(false),
|
||||
: i18n._({
|
||||
id: "email.digest.weekly.subject",
|
||||
message: "Your week on {site}, {range}",
|
||||
values: { site: OG_SITE_NAME, range: range(false) },
|
||||
}),
|
||||
preheader: preheader(content),
|
||||
markdown: md.join("\n\n"),
|
||||
@@ -438,7 +566,14 @@ function preheader(content: DigestContent): string {
|
||||
const n = (content.network?.dumps.length ?? 0) +
|
||||
(content.network?.more ?? 0);
|
||||
if (n > 0) {
|
||||
parts.push(i18n._("email.digest.pre.network", { n }));
|
||||
parts.push(
|
||||
i18n._({
|
||||
id: "email.digest.pre.network",
|
||||
message:
|
||||
"{n, plural, one {# new dump from people you follow} other {# new dumps from people you follow}}",
|
||||
values: { n },
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const members = (content.community?.members.length ?? 0) +
|
||||
@@ -447,18 +582,42 @@ function preheader(content: DigestContent): string {
|
||||
(content.community?.playlistsMore ?? 0);
|
||||
if (content.top.length > 0) {
|
||||
parts.push(
|
||||
i18n._("email.digest.pre.topMonth", { n: content.top.length }),
|
||||
i18n._({
|
||||
id: "email.digest.pre.topMonth",
|
||||
message:
|
||||
"{n, plural, one {the most upvoted dump of the month} other {the # most upvoted dumps of the month}}",
|
||||
values: { n: content.top.length },
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (members > 0) {
|
||||
parts.push(i18n._("email.digest.pre.members", { n: members }));
|
||||
parts.push(
|
||||
i18n._({
|
||||
id: "email.digest.pre.members",
|
||||
message: "{n, plural, one {# new member} other {# new members}}",
|
||||
values: { n: members },
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (playlists > 0) {
|
||||
parts.push(i18n._("email.digest.pre.playlists", { n: playlists }));
|
||||
parts.push(
|
||||
i18n._({
|
||||
id: "email.digest.pre.playlists",
|
||||
message: "{n, plural, one {# new playlist} other {# new playlists}}",
|
||||
values: { n: playlists },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (parts.length === 0 && content.top.length > 0) {
|
||||
parts.push(i18n._("email.digest.pre.topWeek", { n: content.top.length }));
|
||||
parts.push(
|
||||
i18n._({
|
||||
id: "email.digest.pre.topWeek",
|
||||
message:
|
||||
"{n, plural, one {the most upvoted dump of the week} other {the # most upvoted dumps of the week}}",
|
||||
values: { n: content.top.length },
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (parts.length === 0) return "";
|
||||
const line = new Intl.ListFormat(EMAIL_LOCALE, { type: "conjunction" })
|
||||
|
||||
Reference in New Issue
Block a user