add i18n translation hygiene#42
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
There was a problem hiding this comment.
Pull request overview
This PR introduces an i18n “translation hygiene” guard and migrates a large portion of UI copy to next-intl, adding Spanish and German catalogue coverage and wiring CI to enforce the new checks.
Changes:
- Added
npm run i18n:check(structure/key/placeholder/tag validation) and wired it intonpm run check. - Replaced hard-coded UI strings across auth, profile, listings, chat, map, newsletter, and shared components with
next-intltranslations (including rich-text messages). - Expanded
en/es/demessage catalogues and added a GitHub Actions workflow to run checks on PRs andmain.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/ThreadsList/ThreadsList.jsx | Localized chat list title/empty state. |
| src/components/SignUpForm/SignUpForm.tsx | Localized Turnstile + form copy and richer error rendering. |
| src/components/SignInForm/SignInForm.jsx | Localized sign-in labels/buttons and loading text. |
| src/components/ProfileListings/ProfileListings.jsx | Localized listing cards and add-listing prompts. |
| src/components/ProfileHeader/ProfileHeader.jsx | Server-side translation for admin badge. |
| src/components/ProfileActions/ProfileActions.tsx | Localized profile action titles/dialog copy and buttons. |
| src/components/ProfileAccountSettings/ProfileAccountSettings.tsx | Localized account settings labels/hints/actions. |
| src/components/PostageStamp/PostageStamp.jsx | Localized stamp alt text. |
| src/components/NewsletterIssuesList/NewsletterIssuesList.tsx | Server-side translation for section headings. |
| src/components/NewsletterCallout/NewsletterCallout.jsx | Localized newsletter callouts and buttons. |
| src/components/NewsletterAside/NewsletterAside.jsx | Localized newsletter aside with rich links. |
| src/components/MapSidebar/MapSidebar.jsx | Localized sidebar headings/steps/source label. |
| src/components/MapSearch/MapSearch.jsx | Localized MapTiler control placeholder/error/no-results strings. |
| src/components/MapPageClient/MapPageClient.jsx | Localized drawer aria text + “not found”/empty states. |
| src/components/LocationSelect/LocationSelect.jsx | Localized location field labels/hints and map search errors. |
| src/components/ListingWrite/ListingWrite.tsx | Localized listing create/edit form sections, hints, actions. |
| src/components/ListingRead/ListingRead.jsx | Localized listing read sections and CTA/link text. |
| src/components/ListingPhotosManager/ListingPhotosManager.tsx | Localized upload/delete alerts, labels, and alt text. |
| src/components/ListingHeader/ListingHeader.jsx | Localized listing header fallback alt + type/location labels. |
| src/components/ListingCta/ListingCta.jsx | Localized listing CTA messaging (owner/stub/guest). |
| src/components/LegalFooter/LegalFooter.jsx | Localized footer navigation labels. |
| src/components/LegalAgreement/LegalAgreement.jsx | Localized legal agreement with rich links. |
| src/components/Label/Label.jsx | Localized “(optional)” suffix. |
| src/components/IntroHeader/IntroHeader.jsx | Localized avatar alt text. |
| src/components/ChatWindow/ChatWindow.tsx | Localized empty state + thread initiation copy. |
| src/components/ChatHeader/ChatHeader.jsx | Localized drawer aria text + report dialog copy. |
| src/components/ChatComposer/ChatComposer.tsx | Localized placeholder/send labels/loading label. |
| src/components/ButtonToDialog/ButtonToDialog.tsx | Localized default cancel/loading labels. |
| src/components/AvatarUploadView/AvatarUploadView.tsx | Localized upload UI strings and avatar alt text. |
| src/components/AvatarUploadManager/AvatarUploadManager.jsx | Localized upload error alerts. |
| src/app/not-found.js | Server-side localized 404 body + home link label. |
| src/app/actions.ts | Server-side localized error/success messages in actions. |
| src/app/(forms)/sign-in/page.tsx | Server-side localized sign-in page framing. |
| src/app/(forms)/profile/reset-password/page.tsx | Server-side localized reset-password page copy/buttons. |
| src/app/(forms)/profile/listings/new/page.js | Localized listing-type selection flow + localized Suspense fallback. |
| src/app/(forms)/profile/listings/[slug]/page.js | Localized edit-listing header + not-found text. |
| src/app/(forms)/forgot-password/page.tsx | Server-side localized forgot-password page copy/buttons. |
| src/app/(forms)/auth/complete/page.tsx | Server-side localized auth-complete page copy. |
| src/app/(core)/(static)/newsletter/page.tsx | Server-side localized newsletter page headers + RSS rich link. |
| src/app/(core)/(static)/newsletter/(issues)/[slug]/page.tsx | Server-side localized issue subtitle + RSS rich link. |
| src/app/(core)/(interact)/(centered)/profile/page.js | Server-side localized profile section headings. |
| scripts/check-i18n-messages.mjs | New locale consistency/structure/ICU/tag validation script. |
| package.json | Added i18n:check and updated check to run it. |
| messages/en.json | Added new/updated keys for migrated UI strings. |
| messages/es.json | Added Spanish coverage for migrated UI strings. |
| messages/de.json | Added German coverage for migrated UI strings. |
| .github/workflows/validate-app.yml | New CI workflow to run npm run check. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { Label as HeadlessLabel } from "@headlessui/react"; | ||
| import { styled } from "@pigment-css/react"; | ||
| import { useTranslations } from "next-intl"; |
There was a problem hiding this comment.
Label now calls useTranslations, but this file is not marked as a Client Component. Since Label is imported from Server Components (e.g., app route pages), this will fail with a "hooks/useTranslations not supported in Server Components" error. Either add a top-level "use client" directive, or switch this component to the server API (getTranslations) and pass the optional suffix text in as a prop to keep it server-rendered.
| import { useTranslations } from "next-intl"; | ||
|
|
||
| const currentYear = new Date().getFullYear(); | ||
|
|
||
| export default function LegalFooter() { | ||
| const t = useTranslations(); |
There was a problem hiding this comment.
LegalFooter uses the client-only useTranslations hook but is rendered from Server Components (e.g. src/app/(core)/(static)/layout.jsx). This will break Server Component rendering unless the footer is explicitly a Client Component or switched to getTranslations (recommended here to avoid forcing extra hydration).
| import { useTranslations } from "next-intl"; | |
| const currentYear = new Date().getFullYear(); | |
| export default function LegalFooter() { | |
| const t = useTranslations(); | |
| import { getTranslations } from "next-intl/server"; | |
| const currentYear = new Date().getFullYear(); | |
| export default async function LegalFooter() { | |
| const t = await getTranslations(); |
| @@ -125,23 +125,22 @@ export default function ProfileListings({ user, profile, listings }) { | |||
| size="small" | |||
| profile={listing.type === "residential" ? profile : undefined} | |||
| listing={listing.type !== "residential" ? listing : undefined} | |||
| alt={`Your avatar for this ${listing.type} listing`} | |||
| alt={t("Profile.listingCardAlt", { type: listing.type })} | |||
| /> | |||
There was a problem hiding this comment.
Profile.listingCardAlt is interpolating listing.type directly (e.g. "residential", "community"), which is an internal enum value and won’t be localized/capitalized. Consider mapping the listing type to a translated, user-facing label (or use an ICU select in the message keyed by type) before interpolating.
| <h3> | ||
| {listing.type === "residential" | ||
| ? profile.first_name | ||
| : listing.name} | ||
| </h3> | ||
| <p> | ||
| {listing.type.charAt(0).toUpperCase() + listing.type.slice(1)}{" "} | ||
| listing | ||
| </p> | ||
| <p>{t("Profile.listingCardType", { type: listing.type })}</p> | ||
| </Text> |
There was a problem hiding this comment.
Profile.listingCardType interpolates listing.type directly, which will leak internal enum values into the UI (and into non-English locales). Prefer passing a localized display label for the type (or use an ICU select in the translation string).
| {(error || hasFieldErrors) && ( | ||
| <FormMessage | ||
| message={{ | ||
| error: error ? ( | ||
| <> | ||
| {error.endsWith(".") ? error : `${error}.`} If you think this | ||
| might be wrong, please{" "} | ||
| <EncodedEmailLink address={siteConfig.encodedEmail.support}> | ||
| email us | ||
| </EncodedEmailLink> | ||
| . | ||
| </> | ||
| ) : hasFieldErrors ? ( | ||
| "Please fix the above error and then try again." | ||
| ) : ( | ||
| "Hmm, something went wrong. Please try again." | ||
| ), | ||
| error: error | ||
| ? t.rich("Auth.signUp.errorWithSupport", { | ||
| error: error.endsWith(".") ? error : `${error}.`, | ||
| link: (chunks) => ( | ||
| <EncodedEmailLink address={siteConfig.encodedEmail.support}> | ||
| {chunks} | ||
| </EncodedEmailLink> | ||
| ), | ||
| }) | ||
| : hasFieldErrors | ||
| ? t("Errors.validationSummary", { count: 1 }) | ||
| : t("Errors.generic"), |
There was a problem hiding this comment.
Errors.validationSummary supports pluralization via the count param, but this call always passes {count: 1} even though hasFieldErrors can represent multiple field errors (e.g. first name + captcha). Pass the actual number of field errors so the summary message stays accurate in all locales.
| <InputHint variant={error ? "error" : undefined}> | ||
| {error | ||
| ? error | ||
| : `Start typing, then select one of the suggested ${listingType === "residential" ? "options" : "addresses"} from the dropdown.`} | ||
| : t("Listings.form.locationHint", { | ||
| kind: listingType === "residential" ? "options" : "addresses", | ||
| })} | ||
| </InputHint> |
There was a problem hiding this comment.
Listings.form.locationHint receives kind as hard-coded English strings ("options"/"addresses"), which will render untranslated inside non-English messages. Prefer making this a translation concern (e.g. ICU select on listing type) or pass a localized kind value from translations.
| <Label htmlFor="name">{t("Listings.form.placeName")}</Label> | ||
| <InputComponent | ||
| id="name" | ||
| name="name" | ||
| required={true} | ||
| type="text" | ||
| minLength={FIELD_CONFIGS.firstName.minLength} | ||
| placeholder={`Your ${listingType === "business" ? "business’" : `${listingType}’s`} name`} | ||
| placeholder={t("Listings.form.placeNamePlaceholder", { | ||
| type: | ||
| listingType === "business" | ||
| ? "business’" | ||
| : `${listingType}’s`, | ||
| })} |
There was a problem hiding this comment.
Listings.form.placeNamePlaceholder is being given English possessive fragments (e.g. "business’", "community’s") via the type param. This will produce incorrect/untranslated output in es/de. Prefer passing a stable type key (e.g. "business"/"community") and handle the grammar in the message with ICU select (or compute a localized display string via t).
| <Label htmlFor="description" required={false}> | ||
| Short description or instructions | ||
| {t("Listings.form.descriptionLabel")} | ||
| </Label> | ||
| <TextareaComponent | ||
| id="description" | ||
| rows={listingType === "residential" ? 4 : 5} | ||
| maxLength={DESCRIPTION_MAX_CHARACTERS} | ||
| required={false} | ||
| resize="vertical" | ||
| placeholder={`About your ${listingType === "residential" ? "listing" : listingType}`} | ||
| placeholder={t("Listings.form.descriptionPlaceholder", { | ||
| type: listingType === "residential" ? "listing" : listingType, | ||
| })} | ||
| value={description} |
There was a problem hiding this comment.
Listings.form.descriptionPlaceholder is interpolating listingType directly ("community"/"business") and even the literal "listing" string. These values won’t be localized in other languages. Prefer passing a type key and using ICU select in the message, or pass a translated label for the placeholder.
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| - name: Run app checks | ||
| run: npm run check |
There was a problem hiding this comment.
The PR description calls out running npm run build in validation, but this workflow only runs npm run check (currently i18n + prettier). Consider adding a build step (and/or typecheck/lint if applicable), or folding npm run build into npm run check, so CI enforces the stated validation.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 49 out of 49 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (3)
src/components/LocationSelect/LocationSelect.tsx:72
console.log({ features });will spam the console for every reverse-geocode call and can leak location-related data during normal use. Please remove this log (or gate it behind an explicit dev/debug flag).
src/components/PostageStamp/PostageStamp.jsx:16StyledImageis markedaria-hidden="true"/role="presentation", which hides it from assistive tech. Providing a non-empty translatedaltat the same time is inconsistent (the alt text will never be announced). Either removearia-hidden/presentation if the stamp is informative, or keep it decorative and setaltto an empty string (and drop the translation key).
src/components/MultiInput/MultiInput.tsx:20- There are several unused imports here (e.g.
Form,SubmitButton,Textarea, andstyled). Removing them will reduce bundle/compile noise and makes the component’s dependencies clearer.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "mapVisibility": "Sichtbarkeit auf der Karte", | ||
| "showOnMap": "Diesen Eintrag auf der Karte anzeigen", | ||
| "hideFromMap": "Diesen Eintrag von der Karte ausblenden", | ||
| "adminHint": "Admin-only controls for this listing.", |
There was a problem hiding this comment.
This German message value is still English ("Admin-only controls for this listing.") even though it’s now part of the migrated UI copy. Please provide a German translation to avoid mixed-language UI in the de locale.
| "adminHint": "Admin-only controls for this listing.", | |
| "adminHint": "Nur für Admins sichtbare Steuerelemente für diesen Eintrag.", |
Summary
npm run i18n:checkguard for message key coverage, structure, empty strings, ICU placeholders, and rich-text tagsnext-intlacross auth, profile, listings, chat, map, newsletter shell, legal agreement/footer, uploads, and shared actions/errorsmainNotes
Long-form MDX, newsletter archive content, RSS body copy, and wider email/editorial content remain out of scope for this pass.
Validation
npm run i18n:checknpm run checknpm run build