extend Peels i18n across app, content, and emails#55
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Updates to Preview Branch (dnywh/global-i18n-rollout) ↗︎
Tasks are run on every commit but only new migration files are pushed.
View logs for this Workflow Run ↗︎. |
There was a problem hiding this comment.
Pull request overview
Consolidates Peels internationalisation around in-repo next-intl catalogues and extends locale-aware behaviour across the web app, longform content (newsletter/legal), auth redirects, profile preferences, and transactional/newsletter emails.
Changes:
- Adds locale configuration + persistence (cookies, profile
preferred_locale, auth redirect propagation). - Introduces locale-aware content loading with per-locale MDX wrappers + fallback notices for newsletter and legal pages, plus locale-scoped RSS metadata.
- Localises email subjects/bodies for auth flows, chat notifications, and newsletter issues (including Resend audience broadcasts).
Reviewed changes
Copilot reviewed 71 out of 71 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| supabase/migrations/20260421123000_add_profile_preferred_locale.sql | Adds preferred_locale column + constraint and populates it on profile creation. |
| supabase/functions/send-email-for-newsletter-issue-supabase-users/index.ts | Sends locale-aware newsletter issue emails to opted-in Peels users. |
| supabase/functions/send-email-for-newsletter-issue-resend-audience/index.ts | Creates/sends locale-segmented Resend broadcasts for external audiences. |
| supabase/functions/send-email-for-new-chat-message/index.ts | Localises chat notification email subject/body via shared i18n helpers. |
| supabase/functions/send-email-for-auth-action/index.ts | Localises Supabase auth hook emails based on redirect/user preference. |
| supabase/functions/_templates/sign-up-email.tsx | Adds locale-aware copy and passes locale into confirm URLs. |
| supabase/functions/_templates/reset-password-email.tsx | Adds locale-aware copy and passes locale into confirm URLs. |
| supabase/functions/_templates/reauthentication-email.tsx | Adds locale-aware copy for OTP/reauth flows. |
| supabase/functions/_templates/newsletter-issue-email.tsx | New locale-aware newsletter issue email template linking to web issue. |
| supabase/functions/_templates/new-chat-message-email.tsx | Localises chat email template + listing-context string. |
| supabase/functions/_templates/magic-link-email.tsx | Adds locale-aware copy and passes locale into confirm URLs. |
| supabase/functions/_templates/invite-email.tsx | Adds locale-aware copy and passes locale into confirm URLs. |
| supabase/functions/_templates/email-change-email.tsx | Adds locale-aware copy and passes locale into confirm URLs. |
| supabase/functions/_templates/build-auth-confirm-url.ts | Adds optional locale query param to auth confirm URLs. |
| supabase/functions/_shared/newsletter.ts | New shared newsletter config: current issue + per-locale copy + audiences. |
| supabase/functions/_shared/i18n.ts | New shared email i18n: locale resolution + auth/chat copy. |
| src/utils/authRedirects.ts | Adds helpers to read/append locale in auth redirect URLs and resolve auth locale. |
| src/lib/content/utils.ts | Adds locale-aware content imports + locale-based date formatting. |
| src/lib/content/types.ts | Extends content types with locale + isFallback. |
| src/lib/content/handlers/newsletter.ts | Loads newsletter MDX per locale with fallback and cached module access. |
| src/lib/content/handlers/legal.ts | Loads legal MDX per locale with fallback and cached module access. |
| src/i18n/services/locale.ts | Resolves user locale from profile/auth metadata/cookie/Accept-Language; sets cookie. |
| src/i18n/config.ts | Expands supported locales, adds labels, cookie name, normalisation + Accept-Language parsing. |
| src/content/newsletter/pt-BR/celebrating-the-first-few-months.mdx | Adds pt-BR wrapper with translated metadata + fallback notice. |
| src/content/newsletter/pt-BR/beginning-to-look-like-compost.mdx | Adds pt-BR wrapper with translated metadata + fallback notice. |
| src/content/newsletter/fr/celebrating-the-first-few-months.mdx | Adds fr wrapper with translated metadata + fallback notice. |
| src/content/newsletter/fr/beginning-to-look-like-compost.mdx | Adds fr wrapper with translated metadata + fallback notice. |
| src/content/newsletter/es/celebrating-the-first-few-months.mdx | Adds es wrapper with translated metadata + fallback notice. |
| src/content/newsletter/es/beginning-to-look-like-compost.mdx | Adds es wrapper with translated metadata + fallback notice. |
| src/content/newsletter/de/celebrating-the-first-few-months.mdx | Adds de wrapper with translated metadata + fallback notice. |
| src/content/newsletter/de/beginning-to-look-like-compost.mdx | Adds de wrapper with translated metadata + fallback notice. |
| src/content/legal/pt-BR/terms.mdx | Adds pt-BR legal translation. |
| src/content/legal/pt-BR/privacy.mdx | Adds pt-BR legal translation. |
| src/content/legal/fr/terms.mdx | Adds fr legal translation. |
| src/content/legal/fr/privacy.mdx | Adds fr legal translation. |
| src/content/legal/es/terms.mdx | Adds es legal translation. |
| src/content/legal/es/privacy.mdx | Adds es legal translation. |
| src/content/legal/de/terms.mdx | Adds de legal translation. |
| src/content/legal/de/privacy.mdx | Adds de legal translation. |
| src/content/legal/colophon.mdx | Updates translation tooling description to remove Crowdin reference. |
| src/config/site.ts | Removes newsletter description from config (moves to messages). |
| src/components/TranslationNotice/index.ts | Re-export for TranslationNotice component. |
| src/components/TranslationNotice/TranslationNotice.tsx | New UI component to indicate English fallback on translated pages. |
| src/components/Select/Select.jsx | Adds compact variant styling and adjusts padding for dropdown icon. |
| src/components/ProfileAccountSettings/ProfileAccountSettings.tsx | Adds preferred language selector and persists it via server action. |
| src/components/NewsletterIssuesList/NewsletterIssuesList.tsx | Loads issues using current locale and localises tile subtitle. |
| src/components/NewsletterIssueTile/NewsletterIssueTile.tsx | Replaces hardcoded issue/date line with localised subtitle prop. |
| src/components/LocalePicker/index.ts | Re-export for LocalePicker component. |
| src/components/LocalePicker/LocalePicker.tsx | New client locale picker that updates cookie via server action. |
| src/components/LegalFooter/LegalFooter.tsx | Shows locale picker for guests in footer. |
| src/components/DropdownIcon/DropdownIcon.jsx | Adds compact variant sizing/positioning. |
| src/components/AuthHashCompletion.tsx | Propagates locale through auth hash completion/session creation. |
| src/app/layout.tsx | Localises site metadata and RSS alternate link per locale. |
| src/app/auth/session/route.ts | Persists locale cookie during session completion. |
| src/app/auth/confirm/route.ts | Persists locale cookie during OTP confirmation. |
| src/app/auth/callback/route.ts | Persists locale cookie during OAuth code exchange and propagates locale forward. |
| src/app/actions.ts | Adds locale-aware auth redirects; adds actions for display locale + preferred locale. |
| src/app/(forms)/sign-up/page.tsx | Localises page metadata. |
| src/app/(forms)/sign-in/page.tsx | Localises page metadata. |
| src/app/(core)/(static)/newsletter/page.tsx | Localises newsletter page metadata and RSS link by locale. |
| src/app/(core)/(static)/newsletter/feed.xml/route.ts | Makes RSS locale-aware and localises feed metadata/content strings. |
| src/app/(core)/(static)/newsletter/(issues)/[slug]/page.tsx | Loads locale-specific issue module; shows fallback notice when needed. |
| src/app/(core)/(static)/(legal)/[slug]/page.tsx | Loads locale-specific legal module; localises “last updated” + fallback notice. |
| src/app/(core)/(interact)/(centered)/profile/page.js | Localises profile metadata; passes preferred locale into account settings. |
| messages/pt-BR.json | Adds pt-BR next-intl message catalogue. |
| messages/fr.json | Adds fr next-intl message catalogue. |
| messages/es.json | Extends es catalogue for language + newsletter/legal keys. |
| messages/en.json | Extends en catalogue for language + newsletter/legal keys. |
| messages/de.json | Extends de catalogue for language + newsletter/legal keys. |
| crowdin.yml | Removes Crowdin configuration. |
| README.md | Documents in-repo i18n workflow, locale-aware content structure, and newsletter email process. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function isMissingPreferredLocaleColumn(error: { | ||
| code?: string | null; | ||
| message?: string | null; | ||
| }) { | ||
| return ( | ||
| error.code === "PGRST204" && /preferred_locale/i.test(error.message ?? "") | ||
| ); | ||
| } |
There was a problem hiding this comment.
isMissingPreferredLocaleColumn is duplicated in multiple places (e.g. here and src/app/actions.ts). Consider centralising this PostgREST error check in a shared server util to reduce drift and keep behaviour consistent.
| } catch (_error) { | ||
| // Fall through to the default English content. | ||
| } |
There was a problem hiding this comment.
The locale-specific import fallback catches all errors, which can silently hide real problems (e.g., a syntax/runtime error inside a translated MDX file) and unexpectedly serve English instead. Consider only swallowing “module not found” errors and rethrowing/logging other import failures so broken translations fail loudly during dev/build.
| export async function generateMetadata(): Promise<Metadata> { | ||
| const locale = await getLocale(); | ||
| const t = await getTranslations(); | ||
| const description = t("Index.subtitle"); | ||
|
|
There was a problem hiding this comment.
Using getLocale()/getTranslations() in the root generateMetadata() makes metadata request-dependent and can force dynamic rendering (reducing static optimisation/caching across the app). If this isn’t intentional, consider keeping root metadata static (or limiting locale-specific metadata to routes that need it) and/or adding caching/revalidation strategy.
| resolvedLocale, | ||
| "publishDate", | ||
| true // publishDate is required for legal pages | ||
| ); |
There was a problem hiding this comment.
This inline comment looks incorrect in the newsletter handler: it says “publishDate is required for legal pages” but this code path is formatting newsletter issues. Update the comment to avoid confusion.
| export const dynamic = "force-dynamic"; | ||
|
|
There was a problem hiding this comment.
force-dynamic means this RSS feed is generated on every request; with multiple locales this can add avoidable load. Consider adding an explicit caching strategy (e.g., export const revalidate = … and/or Cache-Control with s-maxage) so the feed can be CDN-cached while still supporting the locale query param.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 72 out of 72 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const preferredLocale = | ||
| normaliseLocale(profile?.preferred_locale) ?? | ||
| normaliseLocale(user?.user_metadata?.preferred_locale) ?? |
There was a problem hiding this comment.
normaliseLocale(user?.user_metadata?.preferred_locale) can throw at runtime if preferred_locale is present but not a string (e.g. malformed/malicious user metadata), since normaliseLocale calls .trim(). Guard with a typeof ... === "string" check (like elsewhere in the PR) before passing the value in.
| const preferredLocale = | |
| normaliseLocale(profile?.preferred_locale) ?? | |
| normaliseLocale(user?.user_metadata?.preferred_locale) ?? | |
| const userMetadataPreferredLocale = user?.user_metadata?.preferred_locale; | |
| const preferredLocale = | |
| normaliseLocale(profile?.preferred_locale) ?? | |
| (typeof userMetadataPreferredLocale === "string" | |
| ? normaliseLocale(userMetadataPreferredLocale) | |
| : undefined) ?? |
| return ( | ||
| error.code === "PGRST204" && | ||
| new RegExp(columnName, "i").test(error.message ?? "") | ||
| ); |
There was a problem hiding this comment.
new RegExp(columnName, "i") treats columnName as a regex pattern, so callers with characters like . or - would change the match semantics. If this helper is meant to be generic, escape columnName before building the regex (or use a simple case-insensitive substring check).
| newsletterIssues.forEach((issue) => { | ||
| const issueLink = `${siteConfig.url}/newsletter/${issue.slug}`; |
There was a problem hiding this comment.
The RSS feed is locale-specific (driven by ?locale=), but the issue URLs built from issue.slug don’t preserve that locale. This affects both issueLink (used in the item HTML) and feed.addItem({ link: ... }), so consumers can see localized feed metadata but land on a page rendered in a different language. Consider appending ?locale=${locale} to the generated issue URLs here.
| export const getNewsletterIssueUrl = () => | ||
| `https://www.peels.app/newsletter/${currentIssue.slug}`; |
There was a problem hiding this comment.
getNewsletterIssueUrl always returns a URL without any locale context. Since this URL is used in localized newsletter emails, consider accepting a locale arg and including it in the URL (e.g. as a ?locale= param) so the “view online”/CTA links consistently open the intended language experience.
| const copy = getNewsletterEmailCopy(locale); | ||
| const issue = getCurrentNewsletterIssue(locale); | ||
| const issueUrl = getNewsletterIssueUrl(); | ||
| const profileUrl = "https://www.peels.app/profile"; |
There was a problem hiding this comment.
issueUrl is generated without any locale context, even though the email subject/body is localized. If you want recipients (especially external audience members without a saved locale) to land on the correct language version, pass locale through to the URL (e.g. via getNewsletterIssueUrl(locale)).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 73 out of 73 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ) : ( | ||
| <> | ||
| You’re receiving this email because you originally reached out to{" "} | ||
| {senderName} on{" "} | ||
| <EmailLink href={`${siteUrl}/profile`}>Peels</EmailLink>. | ||
| </> | ||
| copy.initiatorFooter.replace("{senderName}", senderName) | ||
| ) |
There was a problem hiding this comment.
The initiator footer is now rendered as plain text (copy.initiatorFooter...) instead of including a link to Peels (previously an <EmailLink>). This is a UX regression in the email (no clickable link). Consider keeping the link by splitting the copy into before/link/after parts (similar to the owner footer) or by templating a placeholder that can be wrapped in <EmailLink>.
| const feed = new Feed({ | ||
| title: `${siteConfig.name}: ${t("title")}`, | ||
| description: t("description"), | ||
| id: `${siteConfig.url}/newsletter`, | ||
| link: `${siteConfig.url}/newsletter/feed.xml?locale=${locale}`, |
There was a problem hiding this comment.
Now that /newsletter/feed.xml can vary by ?locale=..., the RSS feed id is still the same for all locales (${siteConfig.url}/newsletter). Some RSS readers treat id as the stable identifier and may conflate different locale feeds. Consider making id (and/or title) locale-specific (e.g. include the locale in the id/link) so each localized feed is uniquely identified.
| const feed = new Feed({ | |
| title: `${siteConfig.name}: ${t("title")}`, | |
| description: t("description"), | |
| id: `${siteConfig.url}/newsletter`, | |
| link: `${siteConfig.url}/newsletter/feed.xml?locale=${locale}`, | |
| const feedUrl = `${siteConfig.url}/newsletter/feed.xml?locale=${locale}`; | |
| const feed = new Feed({ | |
| title: `${siteConfig.name}: ${t("title")}`, | |
| description: t("description"), | |
| id: feedUrl, | |
| link: feedUrl, |
| (new.raw_user_meta_data->>'first_name')::text, | ||
| (new.raw_user_meta_data->>'is_newsletter_subscribed')::boolean, | ||
| (new.raw_user_meta_data->>'preferred_locale')::text, | ||
| (new.raw_user_meta_data->>'http_referrer')::text, | ||
| (new.raw_user_meta_data->>'utm_source')::text, | ||
| (new.raw_user_meta_data->>'utm_medium')::text, | ||
| (new.raw_user_meta_data->>'utm_campaign')::text |
There was a problem hiding this comment.
handle_new_user inserts preferred_locale directly from raw_user_meta_data. If that value is anything other than the allowed set (e.g. en-US), the new profiles_preferred_locale_check constraint will reject the row and the trigger will fail, potentially blocking user creation. Consider sanitizing in the trigger (e.g. only insert when it matches the allowed locales, else insert NULL) or normalizing the value before insert.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 73 out of 73 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { data, error } = await resend.emails.send({ | ||
| from: `Peels <${generalEmailAddress}>`, | ||
| to: [recipientEmail], | ||
| subject: `${senderName} just messaged you`, | ||
| subject: copy.subject.replace("{senderName}", senderName), |
There was a problem hiding this comment.
recipientEmail can be undefined (e.g., if the auth admin lookup fails or returns a user without an email), but it’s still passed to resend.emails.send as to: [recipientEmail]. This will cause the send call to fail at runtime.
Add an explicit guard before sending (and ideally treat it as a handled error/early return) when recipientEmail is missing, similar to how the newsletter sender checks userEmail.
| const supabase = await createClient(); | ||
| const { | ||
| data: { user }, | ||
| } = await supabase.auth.getUser(); |
There was a problem hiding this comment.
LegalFooter now calls supabase.auth.getUser() during server rendering just to decide whether to render LocalePicker. This adds an extra Supabase request on every render of the footer (including for guests).
Consider avoiding the Supabase call here (e.g., render LocalePicker unconditionally, or move the signed-in check client-side / infer it from existing cookies) to reduce latency and load.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 73 out of 73 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const supabase = await createClient(); | ||
| const { | ||
| data: { user }, | ||
| } = await supabase.auth.getUser(); | ||
|
|
||
| if (user?.id) { | ||
| const { data: profile, error: profileError } = await supabase | ||
| .from("profiles") | ||
| .select("preferred_locale") | ||
| .eq("id", user.id) | ||
| .single(); | ||
|
|
||
| if (profileError && !isMissingPreferredLocaleColumn(profileError)) { | ||
| console.error( | ||
| "Error loading preferred locale from profile:", | ||
| profileError | ||
| ); | ||
| } | ||
|
|
||
| const profileLocale = normaliseLocale(profile?.preferred_locale ?? null); | ||
| if (profileLocale) { | ||
| return profileLocale; | ||
| } |
There was a problem hiding this comment.
getUserLocale now calls supabase.auth.getUser() (and potentially a profiles query) before checking the locale cookie or the Accept-Language header. Because this function is used by src/i18n/request.ts, this likely adds a Supabase network round-trip to many/most requests (including guests) even when a valid NEXT_LOCALE cookie is already present. Consider reordering the resolution so cookie/header are checked first, and only hitting Supabase when you actually need to (e.g. signed-in user + no valid locale cookie), or caching the profile locale per request.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 79 out of 79 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <p> | ||
| {t.rich("rss", { | ||
| link: (chunks) => ( | ||
| <Link href="/newsletter/feed.xml">{chunks}</Link> | ||
| <Link href={`/newsletter/feed.xml?locale=${locale}`}> | ||
| {chunks} | ||
| </Link> | ||
| ), |
There was a problem hiding this comment.
The RSS link always appends ?locale=${locale}. When locale is the default (e.g. en), this creates a second feed URL (/newsletter/feed.xml?locale=en) that serves the same content as /newsletter/feed.xml, which can lead to duplicate/canonical and caching inconsistencies. Consider omitting the locale query param for the default locale (or only adding it when locale !== defaultLocale).
| <p> | ||
| {t.rich("rss", { | ||
| link: (chunks) => ( | ||
| <Link href="/newsletter/feed.xml">{chunks}</Link> | ||
| <Link href={`/newsletter/feed.xml?locale=${locale}`}> | ||
| {chunks} | ||
| </Link> | ||
| ), |
There was a problem hiding this comment.
Same as the newsletter index page: the RSS link always includes ?locale=${locale}. For the default locale this produces a duplicate feed URL (/newsletter/feed.xml?locale=en) even though the feed handler’s <link>/<id> values are based on /newsletter/feed.xml without the param. Prefer only adding the query param for non-default locales, or ensure the feed handler echoes the requested param consistently.
| const requestUrl = new URL(request.url); | ||
| const requestedLocale = requestUrl.searchParams.get("locale"); | ||
| const locale = normaliseLocale(requestedLocale) ?? defaultLocale; | ||
| const t = await getTranslations({ locale, namespace: "Newsletter" }); | ||
| const newsletterIssues = await getAllNewsletterIssues(locale); | ||
| const feedUrl = new URL("/newsletter/feed.xml", siteConfig.url); | ||
|
|
||
| export async function GET() { | ||
| const newsletterIssues = await getAllNewsletterIssues(); | ||
| if (requestedLocale && locale !== defaultLocale) { | ||
| feedUrl.searchParams.set("locale", locale); | ||
| } | ||
|
|
||
| const feed = new Feed({ | ||
| title: `${siteConfig.name}: ${t("title")}`, | ||
| description: t("description"), | ||
| id: feedUrl.toString(), | ||
| link: feedUrl.toString(), | ||
| favicon: `${siteConfig.url}/favicon.ico`, | ||
| language: locale, | ||
| copyright: `All rights reserved ${new Date().getFullYear()}, ${siteConfig.name}`, | ||
| }); |
There was a problem hiding this comment.
When the request is /newsletter/feed.xml?locale=en, requestedLocale is truthy but locale === defaultLocale, so the generated feedUrl (and therefore <id>/<link>) omits the query param. This makes the returned feed metadata inconsistent with the requested URL and can cause duplicate feed identities. Either (a) only generate links that omit locale for the default locale, or (b) include the locale param whenever it was explicitly requested (including the default).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 79 out of 79 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function formatContentDate(dateString: string, locale: Locale) { | ||
| return new Intl.DateTimeFormat(locale, { | ||
| year: "numeric", | ||
| month: "long", | ||
| day: "numeric", | ||
| }).format(new Date(dateString)); | ||
| } |
There was a problem hiding this comment.
formatContentDate formats an ISO timestamp (e.g. 2025-06-03T09:00:00+10:00) using the server’s default timezone. On platforms that run in UTC, this can shift the rendered calendar day (e.g. June 3 becomes June 2), which will show incorrect publish/updated dates across newsletter/legal pages and RSS metadata. Consider formatting in a fixed timezone (e.g. the content’s intended zone) or parsing as a date-only value before formatting so the displayed day is stable.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 79 out of 79 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const handleChange = (nextLocale: Locale) => { | ||
| startTransition(async () => { | ||
| const formData = new FormData(); | ||
| formData.set("locale", nextLocale); | ||
| await setDisplayLocaleAction(formData); | ||
| router.refresh(); | ||
| }); |
There was a problem hiding this comment.
setDisplayLocaleAction’s return value (and potential thrown errors) is ignored here. If the action fails, the UI will still refresh with no feedback and the promise rejection may go unhandled. Consider wrapping the action call in try/catch and handling { error } results before calling router.refresh() (or showing a hint/message when the locale update fails).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 79 out of 79 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (const { locale, audienceId } of audienceConfigs) { | ||
| const email = NewsletterIssueEmail({ | ||
| locale, | ||
| recipientName: "{{{FIRST_NAME|there}}}", | ||
| externalAudience: true, | ||
| }); | ||
|
|
||
| if (broadcastError) { | ||
| throw broadcastError; | ||
| } | ||
| const { data: broadcast, error: broadcastError } = | ||
| await resend.broadcasts.create({ | ||
| audienceId, | ||
| from: `Danny from Peels <${newsletterEmailAddress}>`, | ||
| subject: getNewsletterEmailSubject(locale), | ||
| react: email, | ||
| text: await render(email, { plainText: true }), | ||
| headers: { |
There was a problem hiding this comment.
The plaintext text body is rendered from an email instance that contains the Resend merge tag ({{{FIRST_NAME|there}}}) as recipientName. If Resend doesn’t substitute merge tags in the text field for broadcasts, recipients will see the raw placeholder in their greeting. Consider rendering the text version with a non-templated fallback name (e.g., "there") while keeping merge tags in the HTML/React version.
| import { cache } from "react"; | ||
| import type { Locale } from "@/i18n/config"; | ||
| import type { NewsletterIssueData } from "../types"; |
There was a problem hiding this comment.
Locale is imported but never used. This adds noise and may trigger lint failures in stricter environments—remove the unused import.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 79 out of 79 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/components/Select/Select.jsx:25
- There’s duplicate styling in
StyledSelect(color: theme.colors.text.ui.primaryis declared twice). Removing the redundant declaration will reduce noise and avoid confusion when tweaking styles later.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function parseAcceptLanguageHeader( | ||
| headerValue: string | null | ||
| ): Locale[] { | ||
| if (!headerValue) { | ||
| return []; | ||
| } | ||
|
|
||
| return headerValue | ||
| .split(",") | ||
| .map((entry) => { | ||
| const [languageTag, ...params] = entry.trim().split(";"); | ||
| const qualityParam = params.find((param) => | ||
| param.trim().startsWith("q=") | ||
| ); | ||
| const quality = Number.parseFloat( | ||
| qualityParam?.trim().replace("q=", "") ?? "1" | ||
| ); | ||
|
|
||
| return { | ||
| quality: Number.isFinite(quality) ? quality : 0, | ||
| locale: normaliseLocale(languageTag), | ||
| }; | ||
| }) | ||
| .filter( | ||
| (entry): entry is { quality: number; locale: Locale } => | ||
| entry.locale !== null | ||
| ) | ||
| .sort((left, right) => right.quality - left.quality) | ||
| .map((entry) => entry.locale); | ||
| } |
There was a problem hiding this comment.
parseAcceptLanguageHeader includes locales even when the header explicitly sets q=0 ("not acceptable"), because entries are filtered only by locale !== null and not by quality > 0. This can cause getUserLocale() to pick a locale the browser said it won’t accept. Filter out entries with quality <= 0 (and consider clamping quality to [0,1]).
| function isMissingPreferredLocaleColumn(error: { | ||
| code?: string | null; | ||
| message?: string | null; | ||
| }) { | ||
| return ( | ||
| error.code === "PGRST204" && /preferred_locale/i.test(error.message ?? "") | ||
| ); | ||
| } |
There was a problem hiding this comment.
isMissingPreferredLocaleColumn is implemented locally here, and a slightly different version exists in send-email-for-new-chat-message/index.ts. To keep behavior consistent (and avoid regex vs. includes drift), consider moving this helper into a shared module under supabase/functions/_shared/ and importing it in both edge functions.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 80 out of 80 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/components/SiteFooter/index.js:1
src/components/SiteFooter/index.jsre-exports./SiteFooter, but this PR doesn’t add aSiteFootermodule in that directory. This will cause a runtime/build failure when importing@/components/SiteFooter. Fix by either addingsrc/components/SiteFooter/SiteFooter.tsx|jsx(and moving/duplicating the footer implementation there) or changing this barrel to re-export the existing implementation file.
src/i18n/services/locale.ts:1supabase.auth.getUser()typically performs a network call to validate the JWT, which can add latency on every request where an auth cookie is present. If you only needuser_metadata.preferred_locale, consider usingsupabase.auth.getSession()first (session read from cookies) and only falling back togetUser()/ DB lookups when necessary; this keeps locale resolution cheaper on the hot path.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
next-intlmessage cataloguesTesting
npm run i18n:checknpm run checknpm run buildNotes
NEWSLETTER_AUDIENCE_ID_EN,NEWSLETTER_AUDIENCE_ID_ES,NEWSLETTER_AUDIENCE_ID_DE,NEWSLETTER_AUDIENCE_ID_PT_BR, andNEWSLETTER_AUDIENCE_ID_FR