Skip to content

add i18n translation hygiene#42

Merged
dnywh merged 7 commits into
mainfrom
dnywh/i18n-translation-hygiene
Apr 13, 2026
Merged

add i18n translation hygiene#42
dnywh merged 7 commits into
mainfrom
dnywh/i18n-translation-hygiene

Conversation

@dnywh
Copy link
Copy Markdown
Owner

@dnywh dnywh commented Apr 13, 2026

Summary

  • add an in-repo npm run i18n:check guard for message key coverage, structure, empty strings, ICU placeholders, and rich-text tags
  • migrate the main app UI copy into next-intl across auth, profile, listings, chat, map, newsletter shell, legal agreement/footer, uploads, and shared actions/errors
  • add Spanish and German catalogue coverage for the migrated UI copy
  • run the new app validation workflow on PRs and pushes to main

Notes

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:check
  • npm run check
  • npm run build

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peels Ready Ready Preview, Comment Apr 13, 2026 9:41pm

@supabase
Copy link
Copy Markdown

supabase Bot commented Apr 13, 2026

This pull request has been ignored for the connected project mfnaqdyunuafbwukbbyr because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 into npm run check.
  • Replaced hard-coded UI strings across auth, profile, listings, chat, map, newsletter, and shared components with next-intl translations (including rich-text messages).
  • Expanded en/es/de message catalogues and added a GitHub Actions workflow to run checks on PRs and main.

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.

Comment thread src/components/Label/Label.jsx Outdated
Comment on lines +1 to +3
import { Label as HeadlessLabel } from "@headlessui/react";
import { styled } from "@pigment-css/react";
import { useTranslations } from "next-intl";
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +10
import { useTranslations } from "next-intl";

const currentYear = new Date().getFullYear();

export default function LegalFooter() {
const t = useTranslations();
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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();

Copilot uses AI. Check for mistakes.
Comment on lines 124 to 129
@@ -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 })}
/>
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 131 to 137
<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>
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 321 to +335
{(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"),
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 310 to 316
<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>
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +396 to +408
<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`,
})}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines 466 to 478
<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}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +29
- name: Install dependencies
run: npm ci

- name: Run app checks
run: npm run check
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:16
  • StyledImage is marked aria-hidden="true"/role="presentation", which hides it from assistive tech. Providing a non-empty translated alt at the same time is inconsistent (the alt text will never be announced). Either remove aria-hidden/presentation if the stamp is informative, or keep it decorative and set alt to 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, and styled). 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.

Comment thread messages/de.json Outdated
"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.",
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"adminHint": "Admin-only controls for this listing.",
"adminHint": "Nur für Admins sichtbare Steuerelemente für diesen Eintrag.",

Copilot uses AI. Check for mistakes.
@dnywh dnywh merged commit 74e913e into main Apr 13, 2026
5 checks passed
@dnywh dnywh deleted the dnywh/i18n-translation-hygiene branch April 13, 2026 21:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants