-
Notifications
You must be signed in to change notification settings - Fork 824
Showcasing a multilingual newsletter signup form(closes #1761) #1869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…d project structure
📝 WalkthroughWalkthroughAdds a new multilingual newsletter form demo under Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Browser
participant Middleware as next-intl Middleware
participant NextServer as Next.js Server
participant Locales as Locales (files)
participant API as Simulated API
Browser->>Middleware: Request /{locale}/
Middleware->>NextServer: forward request with locale
NextServer->>Locales: import ../locales/{locale}.json
Locales-->>NextServer: messages JSON
NextServer-->>Browser: render page with messages (NewsletterForm + LanguageSwitcher)
Browser->>Browser: user opens LanguageSwitcher (Ctrl/Cmd+L) -> selects locale
Browser->>NextServer: navigate to new locale route (load messages)
Browser->>Browser: user fills form and submits
Browser->>API: POST (simulated) payload
API-->>Browser: 200 OK after SIMULATED_API_DELAY
Browser->>Browser: show SuccessMessage
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 14
🤖 Fix all issues with AI agents
In @.changeset/happy-horses-read.md:
- Around line 1-5: Change the frontmatter release type for the "lingo.dev"
package from major to minor since the addition is non-breaking; update the top
YAML block in .changeset/happy-horses-read.md by replacing "lingo.dev": major
with "lingo.dev": minor so the changeset reflects a minor bump rather than a
major release.
In `@community/multilingual-form-demo/.example.env`:
- Line 1: The LINGODOTDEV_API_KEY placeholder contains an extra quote (""" )
which can break .env parsing; update the LINGODOTDEV_API_KEY entry to use a
standard empty quoted value (two quotes) so it reads LINGODOTDEV_API_KEY="" and
remove the third quote; verify the LINGODOTDEV_API_KEY line is the only one
affected.
In `@community/multilingual-form-demo/app/`[locale]/layout.tsx:
- Line 6: Replace the hard-coded locales array in layout.tsx with the canonical
list exported from the shared types module used by the language switcher
(replace the local const locales = ["en", "es", "de", "fr"] with an import from
languageSwitcher/types.ts), so generateStaticParams and the locale validation
(the check that calls notFound()) will include ja, zh, ko, ar and avoid 404s;
ensure the imported symbol name (e.g., locales or SUPPORTED_LOCALES) is used by
generateStaticParams and the validation logic in this file.
In `@community/multilingual-form-demo/app/components/languageSwitcher/index.tsx`:
- Around line 45-52: The keyboard shortcut callback passed to
useKeyboardShortcut captures a stale isOpen value because the dependency array
is empty; update the handler to use the functional state form so it reads the
latest state (e.g., call setIsOpen(prev => { const next = !prev; if (!prev)
setFocusedIndex(0); return next; }) or otherwise compute new open state inside
the functional updater) instead of referencing isOpen directly, ensuring
setFocusedIndex(0) runs only when opening; modify the callback tied to
useKeyboardShortcut, referencing setIsOpen and setFocusedIndex, to use this
functional update pattern.
- Around line 61-63: The current computation of pathWithoutLocale using
pathname.replace(`/${currentLocale}`, "") can remove occurrences of the locale
anywhere in the path; change it to only strip a leading locale segment by
checking pathname startsWith `/${currentLocale}` (or use a regex that matches
^/locale(?:/|$)) or split pathname by '/' and drop the first segment when it
equals currentLocale, then rejoin—update the logic around pathWithoutLocale and
newPath (referencing pathname, currentLocale, pathWithoutLocale, newPath) so
only the leading locale segment is removed.
In `@community/multilingual-form-demo/app/components/NewsletterForm.tsx`:
- Around line 58-64: The onSubmit handler in NewsletterForm (function onSubmit,
param NewsletterFormData) currently logs raw form data including PII via
console.log after the simulated delay (SIMULATED_API_DELAY); remove that raw
logging or replace it with a development-only, redacted log (e.g., only log
field names or mask email/phone) and ensure production builds never print PII;
update the onSubmit body to remove or gate console.log and keep existing state
updates (setIsSubmitting, setIsSubmitted) intact.
- Around line 106-112: The hard-coded English note "All fields with * are
required" in the NewsletterForm component should be moved into i18n translations
and referenced via the t() helper; replace the literal string in the span
(inside NewsletterForm.tsx where t("validation.demo") is used) with a
translation key like t("validation.requiredFields") and add corresponding
entries for each locale in your translation files (e.g., en, es, etc.) so the
demo remains fully multilingual.
In
`@community/multilingual-form-demo/app/components/newsletterForm/FormCheckbox.tsx`:
- Around line 5-15: FormCheckbox currently always spreads register into the
input and ignores controlled props (checked, onChange) declared on
CheckboxFieldProps; update FormCheckbox to conditionally wire the input so that
if register is provided you spread register, otherwise set the input's checked
and onChange from the props (and keep id, className, etc.). Locate the
FormCheckbox function and modify the <input> handling to prefer register when
present (e.g., {...register}) and fall back to controlled attributes
checked={checked} and onChange={onChange} so both controlled and registered
usages work.
In
`@community/multilingual-form-demo/app/components/newsletterForm/SuccessMessage.tsx`:
- Around line 52-63: The two buttons in SuccessMessage.tsx (the elements using
onReset/ctaText and onHome/homeText) lack explicit type attributes and may
default to type="submit" when nested in a form; update both button elements to
include type="button" to prevent unintended form submissions (add type="button"
to the button with onReset and to the button with onHome).
In `@community/multilingual-form-demo/app/constants/form.constants.ts`:
- Line 16: The PHONE_REGEX currently limits total characters rather than digit
count; update validation so digit count is enforced: either replace PHONE_REGEX
with a pattern that uses a lookahead to require 10–15 digits while allowing
formatting characters (e.g., a lookahead counting \d occurrences and a character
class permitting digits, spaces, +, -, parentheses), or change the form logic
that uses PHONE_REGEX to strip non-digits from the input (e.g., remove
everything except 0-9) and then validate the resulting digit-only string length
between 10 and 15; locate the PHONE_REGEX constant in form.constants.ts and
apply one of these fixes.
In `@community/multilingual-form-demo/app/hooks/useFormValidation.ts`:
- Around line 15-20: The phone formatter formatPhoneNumber currently truncates
after 10 digits causing loss when validation allows up to 15; update
formatPhoneNumber so the final branch preserves any extra digits instead of
slicing to index 10 (use numbers.slice(6) for the tail) so the formatted string
shows all digits beyond the area/exchange parts, keeping the existing
early-return branches for <=3 and <=6 intact and thus aligning formatting with
the validator that permits up to 15 digits.
In `@community/multilingual-form-demo/app/hooks/useKeyboardShortcut.ts`:
- Around line 9-14: The handleKeyDown in useKeyboardShortcut.ts currently checks
(e.altKey || e.metaKey) which mismatches the README's Ctrl+L shortcut; update
the condition to check for Ctrl on non-Mac and Meta on Mac by replacing
(e.altKey || e.metaKey) with (e.ctrlKey || e.metaKey) (or use a platform-aware
check if you need to distinguish further), keeping the rest of the logic
(e.preventDefault() and memoizedCallback()) unchanged and ensure the check uses
e.key.toLowerCase() === key.toLowerCase().
In `@community/multilingual-form-demo/proxy.ts`:
- Around line 6-7: The middleware matcher in export const config (matcher)
currently only includes '/','/(de|en|es|fr)/:path*' and thus omits ja/zh/ko/ar;
update the matcher to include all supported locales (de,en,es,fr,ja,zh,ko,ar) so
locale-prefixed routes are handled (e.g., change the pattern to
'/','/(de|en|es|fr|ja|zh|ko|ar)/:path*' or replace with a single regex that
covers all locales) in proxy.ts where export const config is defined.
In `@community/multilingual-form-demo/README.md`:
- Around line 53-107: The README's step count is wrong—header says "3 steps" but
the document has four actions; update the top sentence to "Adding a new language
requires 4 steps" (or merge "Restart & Test" into step 3 and renumber) and
ensure consistency across references to i18n.json, locales/pt.json creation,
i18n/routing.ts (routing), app/components/LanguageSwitcher/types.ts (locales
array), and the final pnpm dev restart/test instruction so the numbered steps
match the actual sections.
🧹 Nitpick comments (11)
community/multilingual-form-demo/README.md (1)
16-26: Add language specifier to fenced code block.Per static analysis hint, fenced code blocks should have a language specified for proper syntax highlighting.
Proposed fix
-``` +```text ├── locales/ # Translation JSON files (en.json, es.json, fr.json, etc.)community/multilingual-form-demo/locales/zh.json (1)
79-88: Inconsistent language name translations.In the
LocaleSwitcher.languagessection, "en" and "es" are left in English ("English", "Spanish") while the other language names are translated to Chinese (法语, 德语, etc.). For consistency, consider translating all language names to Chinese (e.g., "英语", "西班牙语") or keeping all in their native/English forms.Suggested fix for consistency
"languages": { - "en": "English", - "es": "Spanish", + "en": "英语", + "es": "西班牙语", "fr": "法语", "de": "德语", "ja": "日语", "zh": "中文", "ko": "韩语", "ar": "阿拉伯语" }community/multilingual-form-demo/locales/fr.json (1)
115-115: Untranslated text in footer.The footer contains "Multilingual Form Demo" in English, while other locales have this phrase translated (e.g., Spanish: "Demo de formulario multilingüe", German: "Mehrsprachiges Formular-Demo"). The title at line 91 already uses the French translation "Démo de formulaire multilingue".
Suggested fix
- "footer": "© 2026 Multilingual Form Demo. Créé avec ❤️ en utilisant Next.js" + "footer": "© 2026 Démo de formulaire multilingue. Créé avec ❤️ en utilisant Next.js"community/multilingual-form-demo/app/components/newsletterForm/FormIcons.tsx (1)
1-54: Hide decorative SVGs from assistive tech.If these icons are purely decorative (labels already exist), mark them as hidden to reduce screen‑reader noise.
♿ Suggested tweak
export const UserIcon = () => ( <svg className="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20" + aria-hidden="true" + focusable="false" > @@ export const EmailIcon = () => ( <svg className="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20" + aria-hidden="true" + focusable="false" > @@ export const PhoneIcon = () => ( <svg className="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20" + aria-hidden="true" + focusable="false" > @@ export const InfoIcon = () => ( - <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"> + <svg + className="w-5 h-5" + fill="currentColor" + viewBox="0 0 20 20" + aria-hidden="true" + focusable="false" + > @@ export const LockIcon = () => ( - <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> + <svg + className="w-4 h-4" + fill="currentColor" + viewBox="0 0 20 20" + aria-hidden="true" + focusable="false" + >community/multilingual-form-demo/app/components/newsletterForm/FormInput.tsx (1)
18-70: Associate helper/error text with the input for screen readers.
Right now helper text isn’t referenced byaria-describedby, so it won’t be announced.♿ Proposed fix
export function FormInput({ error, label, id, placeholder, type = "text", icon, register, helperText, }: InputFieldProps) { + const helperId = helperText ? `${id}-help` : undefined; + const errorId = error ? `${id}-error` : undefined; + const describedBy = + [helperId, errorId].filter(Boolean).join(" ") || undefined; + return ( <div> <div className="flex items-center justify-between mb-2"> <label htmlFor={id} className="block text-sm font-semibold text-gray-700 dark:text-gray-300" > {label} </label> {helperText && ( - <div className="text-xs text-gray-500 dark:text-gray-400"> + <div id={helperId} className="text-xs text-gray-500 dark:text-gray-400"> {helperText} </div> )} </div> <div className="relative"> <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> {icon} </div> <input {...register} type={type} id={id} className={`w-full pl-10 pr-4 py-3 rounded-lg border ${ error ? "border-red-300 focus:border-red-500 focus:ring-2 focus:ring-red-200 dark:focus:ring-red-900" : "border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-900" } bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 transition-all duration-200`} placeholder={placeholder} aria-invalid={error ? "true" : "false"} - aria-describedby={error ? `${id}-error` : undefined} + aria-describedby={describedBy} /> </div> <AnimatePresence> {error && ( <motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} - id={`${id}-error`} + id={errorId} className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-2" >community/multilingual-form-demo/app/components/newsletterForm/PrivacyCheckbox.tsx (1)
20-70: Link the error message to the checkbox for screen readers.♿ Proposed fix
<input type="checkbox" id="privacy" {...register} className="peer sr-only" + aria-invalid={error ? "true" : "false"} + aria-describedby={error ? "privacy-error" : undefined} /> @@ {error && ( <motion.p initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} + id="privacy-error" className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-2" >community/multilingual-form-demo/app/[locale]/page.tsx (1)
5-11: Use synchronousparamstyping (no Promise).
Next.js passesparamsas a plain object; awaiting a Promise here is unnecessary and non‑idiomatic.♻️ Suggested refactor
-export default async function HomePage({ - params, -}: { - params: Promise<{ locale: string }>; -}) { - const { locale } = await params; +export default async function HomePage({ + params, +}: { + params: { locale: string }; +}) { + const { locale } = params;community/multilingual-form-demo/app/components/languageSwitcher/types.ts (1)
1-17: LGTM! Consider stricter typing for locale codes.The interface and data structure are well-defined. For improved type safety, you could derive the
codetype from the array itself:💡 Optional type-safety improvement
-// types.ts -export interface LocaleInfo { - code: string; +export const locales = [ + { code: "en", name: "English", flag: "🇺🇸" }, + { code: "es", name: "Español", flag: "🇪🇸" }, + { code: "fr", name: "Français", flag: "🇫🇷" }, + { code: "de", name: "Deutsch", flag: "🇩🇪" }, + { code: "ja", name: "日本語", flag: "🇯🇵" }, + { code: "zh", name: "中文", flag: "🇨🇳" }, + { code: "ko", name: "한국어", flag: "🇰🇷" }, + { code: "ar", name: "العربية", flag: "🇸🇦" }, +] as const; + +export type LocaleCode = typeof locales[number]["code"]; + +export interface LocaleInfo { + code: LocaleCode; name: string; flag: string; }community/multilingual-form-demo/app/components/newsletterForm/PhoneInput.tsx (1)
74-84: Consider extracting the error icon to FormIcons for consistency.The phone icon is imported from
FormIcons, but the error icon is defined inline. Extracting it would improve consistency and reusability across form components.community/multilingual-form-demo/app/types/form.types.ts (2)
1-1: Consider importing React types explicitly.
React.ReactNodeis used on line 29, butReactis not imported. While this works with the new JSX transform and@types/react19's global types, adding an explicit import improves clarity and IDE support.Suggested fix
-import { UseFormRegisterReturn } from "react-hook-form"; +import type { ReactNode } from "react"; +import type { UseFormRegisterReturn } from "react-hook-form";Then update line 29:
- icon: React.ReactNode; + icon: ReactNode;
36-50: Inconsistent base interface extension for form field props.
InputFieldPropsextendsFormFieldProps, butCheckboxFieldPropsandSelectFieldPropsdo not. This creates inconsistency:
CheckboxFieldPropsduplicatesidandlabelbut omitserrorSelectFieldPropsis missingid,label, anderrorentirelyIf these components need to display validation errors, consider extending
FormFieldPropsor adding theerrorprop where needed.
community/multilingual-form-demo/app/components/languageSwitcher/index.tsx
Show resolved
Hide resolved
community/multilingual-form-demo/app/components/languageSwitcher/index.tsx
Outdated
Show resolved
Hide resolved
community/multilingual-form-demo/app/hooks/useFormValidation.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@community/multilingual-form-demo/app/components/NewsletterForm.tsx`:
- Around line 165-181: The privacy links inside privacyText (the t.rich call in
NewsletterForm's JSX) use the plain Next.js Link with absolute paths (/terms,
/privacy) which bypasses localePrefix; import and use the locale-aware Link from
next-intl (replace the current Link import with Link from 'next-intl/link') and
keep the same href values inside the terms/privacy renderers so the next-intl
Link will automatically prepend the current locale and preserve locale-aware
routing.
In `@community/multilingual-form-demo/proxy.ts`:
- Around line 6-8: The file export const config in proxy.ts won't be picked up
by Next.js because it requires middleware.ts; rename proxy.ts to middleware.ts
or create a new middleware.ts that re-exports the config from proxy.ts (e.g.
import { config } from './proxy' and export it) so Next.js auto-detects the
middleware and the matcher ['/','/(de|en|es|fr|ja|zh|ko|ar)/:path*'] becomes
active.
♻️ Duplicate comments (2)
community/multilingual-form-demo/app/components/NewsletterForm.tsx (2)
58-64: Avoid logging raw form data (PII) in the client.
This can leak emails/phones in production; remove or gate/redact it.🔒 Proposed fix (dev-only redacted logging)
- console.log("Form data submitted:", data); + if (process.env.NODE_ENV !== "production") { + console.log("Form submitted", { fields: Object.keys(data) }); + }
106-112: Localize the required-fields note.
This is hard-coded English and should live in translations.🌍 Proposed fix
- {t("validation.demo")}: All fields with * are required + {t("validation.demo")}: {t("validation.requiredFields")}
coderabbitai comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In @.changeset/happy-horses-read.md:
- Line 5: Edit the changeset description string "Added multilingual form for
sign up of newsletter with validation for demo with next-intl and Lingo.dev" to
tighten wording and hyphenate "sign-up" (e.g., "Added multilingual sign-up form
for newsletter with validation for demo using next-intl and Lingo.dev"); ensure
the revised sentence is concise and grammatically correct wherever the changeset
content is defined.
In `@community/multilingual-form-demo/README.md`:
- Around line 16-26: Update the README.md project structure code block to
declare a language for the fenced block (e.g., ```text) and fix the folder name
casing to match the repository by replacing "LanguageSwitcher" with
"languageSwitcher" so it reflects the actual path used by the project (the block
that lists locales/, i18n/, i18n.json and app/components/ -> languageSwitcher/
-> types.ts).
♻️ Duplicate comments (1)
community/multilingual-form-demo/README.md (1)
55-92: Fix the step count to match the four steps listed.Line 55 says “3 steps” but the section has four numbered steps.
📎 Suggested fix
-Adding a new language requires 3 steps: +Adding a new language requires 4 steps:
🧹 Nitpick comments (1)
community/multilingual-form-demo/app/components/NewsletterForm.tsx (1)
78-89: Consider using router navigation instead of page reload.Line 86 uses
window.location.reload()for the "home" action, which bypasses client-side navigation and causes a full page refresh. For a smoother UX consistent with next-intl's locale-aware routing, consider using the router:🔧 Suggested improvement
+"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { motion } from "framer-motion"; +import { Link, useRouter } from "@/i18n/routing"; ... export default function NewsletterForm() { + const router = useRouter(); ... onHome={() => window.location.reload()} + onHome={() => router.push("/")}
Summary
Added a community demo showcasing a multilingual newsletter signup form with AI-powered translations using next-intl and Lingo.dev.
Changes
community/multilingual-form-demo/Testing
Business logic tests added:
Visuals
Required for UI/UX changes:
Checklist
Closes #1761
Summary by CodeRabbit
New Features
Documentation
Chores
#1761