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:
@@ -10,7 +10,8 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { Trans as Message } from "@lingui/react";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import { DIGEST_MESSAGES } from "../../../shared/digest-messages.ts";
|
||||
import { Avatar } from "../shared/Avatar.tsx";
|
||||
import type { SelfUser } from "../../../shared/user.ts";
|
||||
import { can } from "../../utils/auth/permissions.ts";
|
||||
@@ -29,6 +30,7 @@ export interface UserMenuProps {
|
||||
* Escape closes it.
|
||||
*/
|
||||
export function UserMenu({ user }: UserMenuProps): React.JSX.Element {
|
||||
const { i18n } = useLingui();
|
||||
const { logout } = useAuth();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -91,7 +93,7 @@ export function UserMenu({ user }: UserMenuProps): React.JSX.Element {
|
||||
role="menuitem"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Message id="digest.community" />
|
||||
{i18n._(DIGEST_MESSAGES.community)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/settings"
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { i18n } from "@lingui/core";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { Trans as Message } from "@lingui/react";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import { DIGEST_MESSAGES } from "../../shared/digest-messages.ts";
|
||||
import type { Wire } from "../../shared/api.ts";
|
||||
import type { CommunityStats } from "../../shared/community.ts";
|
||||
import type { DigestCadence } from "../../shared/settings.ts";
|
||||
@@ -43,14 +43,24 @@ function isQuiet(stats: CommunityStats): boolean {
|
||||
|
||||
/** Renders the period's four figures, each over its label. */
|
||||
function Figures({ stats }: { stats: CommunityStats }) {
|
||||
const { i18n } = useLingui();
|
||||
const { dumps, upvotes, comments } = stats.totals;
|
||||
const figures: [number, string][] = [
|
||||
[dumps, i18n._("digest.stat.dumps", { n: dumps })],
|
||||
[upvotes, i18n._("digest.count.upvotes", { n: upvotes })],
|
||||
[comments, i18n._("digest.count.comments", { n: comments })],
|
||||
[dumps, i18n._({ ...DIGEST_MESSAGES.statDumps, values: { n: dumps } })],
|
||||
[
|
||||
upvotes,
|
||||
i18n._({ ...DIGEST_MESSAGES.countUpvotes, values: { n: upvotes } }),
|
||||
],
|
||||
[
|
||||
comments,
|
||||
i18n._({ ...DIGEST_MESSAGES.countComments, values: { n: comments } }),
|
||||
],
|
||||
[
|
||||
stats.chatMessages,
|
||||
i18n._("digest.stat.chat", { n: stats.chatMessages }),
|
||||
i18n._({
|
||||
...DIGEST_MESSAGES.statChat,
|
||||
values: { n: stats.chatMessages },
|
||||
}),
|
||||
],
|
||||
];
|
||||
return (
|
||||
@@ -70,7 +80,8 @@ function Figures({ stats }: { stats: CommunityStats }) {
|
||||
* A view no period names is `NotFound`.
|
||||
*/
|
||||
export function Community(): React.JSX.Element {
|
||||
useDocumentTitle(i18n._("digest.community"));
|
||||
const { i18n } = useLingui();
|
||||
useDocumentTitle(i18n._(DIGEST_MESSAGES.community));
|
||||
const { user, authFetch } = useAuth();
|
||||
const [period, setPeriod] = useTabParam<DigestCadence>(
|
||||
communityPath(),
|
||||
@@ -97,7 +108,7 @@ export function Community(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<PageShell narrow>
|
||||
<PageHeader icon="🏘️" title={<Message id="digest.community" />} />
|
||||
<PageHeader icon="🏘️" title={i18n._(DIGEST_MESSAGES.community)} />
|
||||
<TabBar
|
||||
tabs={[
|
||||
{ key: "weekly", label: <Trans>This week</Trans> },
|
||||
@@ -122,15 +133,15 @@ export function Community(): React.JSX.Element {
|
||||
<Figures stats={data} />
|
||||
{data.top.length > 0 && (
|
||||
<PageGroup
|
||||
label={weekly
|
||||
? <Message id="digest.topWeek" />
|
||||
: <Message id="digest.topMonth" />}
|
||||
label={i18n._(
|
||||
weekly ? DIGEST_MESSAGES.topWeek : DIGEST_MESSAGES.topMonth,
|
||||
)}
|
||||
>
|
||||
<DumpCardList dumps={data.top} />
|
||||
</PageGroup>
|
||||
)}
|
||||
{data.members.length > 0 && (
|
||||
<PageGroup label={<Message id="digest.newMembers" />}>
|
||||
<PageGroup label={i18n._(DIGEST_MESSAGES.newMembers)}>
|
||||
<ul className="user-card-list">
|
||||
{data.members.map((m) => (
|
||||
<UserCard
|
||||
@@ -138,20 +149,18 @@ export function Community(): React.JSX.Element {
|
||||
user={m}
|
||||
note={m.invitedByUsername &&
|
||||
(m.invitedByUsername === user?.username
|
||||
? <Message id="digest.invitedByYou" />
|
||||
: (
|
||||
<Message
|
||||
id="digest.invitedBy"
|
||||
values={{ username: m.invitedByUsername }}
|
||||
/>
|
||||
))}
|
||||
? i18n._(DIGEST_MESSAGES.invitedByYou)
|
||||
: i18n._({
|
||||
...DIGEST_MESSAGES.invitedBy,
|
||||
values: { username: m.invitedByUsername },
|
||||
}))}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</PageGroup>
|
||||
)}
|
||||
{data.playlists.length > 0 && (
|
||||
<PageGroup label={<Message id="digest.newPlaylists" />}>
|
||||
<PageGroup label={i18n._(DIGEST_MESSAGES.newPlaylists)}>
|
||||
<PlaylistCardList playlists={data.playlists} />
|
||||
</PageGroup>
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,8 @@ import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
// A label shared with the emails is named by its id, its words being in
|
||||
// locales/ only; the macro's Trans takes the English in place.
|
||||
import { Trans as Message } from "@lingui/react";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import { DIGEST_MESSAGES } from "../../shared/digest-messages.ts";
|
||||
import type { ColorScheme, StyleName } from "../../shared/theme.ts";
|
||||
import type { FeedTab } from "../../shared/paths.ts";
|
||||
import { useTheme } from "../hooks/theme/useTheme.ts";
|
||||
@@ -67,6 +68,7 @@ const ON_OFF_OPTIONS: SegmentedOption<boolean>[] = [
|
||||
* default feed tab and sounds, for now, to this browser.
|
||||
*/
|
||||
export function Settings(): React.JSX.Element {
|
||||
const { i18n } = useLingui();
|
||||
useDocumentTitle(t`Settings`);
|
||||
const { user } = useRequiredAuth();
|
||||
const { style, colorScheme, setStyle, setColorScheme } = useTheme();
|
||||
@@ -142,7 +144,7 @@ export function Settings(): React.JSX.Element {
|
||||
</PageGroup>
|
||||
<PageGroup label={<Trans>Email</Trans>}>
|
||||
<div className="page-card profile-appearance-grid">
|
||||
<SettingRow label={<Message id="digest.weekly" />}>
|
||||
<SettingRow label={i18n._(DIGEST_MESSAGES.weekly)}>
|
||||
<Segmented
|
||||
options={ON_OFF_OPTIONS}
|
||||
value={settings?.digestWeekly ?? true}
|
||||
@@ -151,7 +153,7 @@ export function Settings(): React.JSX.Element {
|
||||
updateSettings({ digestWeekly: on }).catch(() => {})}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label={<Message id="digest.monthly" />}>
|
||||
<SettingRow label={i18n._(DIGEST_MESSAGES.monthly)}>
|
||||
<Segmented
|
||||
options={ON_OFF_OPTIONS}
|
||||
value={settings?.digestMonthly ?? true}
|
||||
|
||||
@@ -11,7 +11,8 @@ import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
// A label shared with the emails is named by its id, its words being in
|
||||
// locales/ only; the macro's Trans takes the English in place.
|
||||
import { Trans as Message } from "@lingui/react";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import { DIGEST_MESSAGES } from "../../shared/digest-messages.ts";
|
||||
import { FormProvider } from "react-hook-form";
|
||||
|
||||
import { API_URL } from "../config/api.ts";
|
||||
@@ -30,6 +31,7 @@ import { readUnsubscribeCadence } from "../utils/auth/unsubscribe.ts";
|
||||
* email, then posts the token to `POST /api/unsubscribe`.
|
||||
*/
|
||||
export function Unsubscribe(): React.JSX.Element {
|
||||
const { i18n } = useLingui();
|
||||
useDocumentTitle(t`Unsubscribe`);
|
||||
const [params] = useSearchParams();
|
||||
const { user } = useAuth();
|
||||
@@ -113,9 +115,9 @@ export function Unsubscribe(): React.JSX.Element {
|
||||
<form onSubmit={onSubmit} className="form">
|
||||
<FormError title={t`Couldn't unsubscribe`} />
|
||||
<SubmitButton pendingLabel={<Trans>Turning off…</Trans>}>
|
||||
{weekly
|
||||
? <Message id="digest.weekly.off" />
|
||||
: <Message id="digest.monthly.off" />}
|
||||
{i18n._(
|
||||
weekly ? DIGEST_MESSAGES.weeklyOff : DIGEST_MESSAGES.monthlyOff,
|
||||
)}
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
Reference in New Issue
Block a user