Add i18n with language selector (12 languages, cookie-based)#15
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds internationalization to the v5 app using next-intl in the cookie-based (no URL prefix) variant. A new top-nav language selector lets users pick from 12 languages; the choice is persisted in a NEXT_LOCALE cookie, with Accept-Language fallback on first visit. RTL layout is supported for Arabic and Hebrew, and the chat API receives the locale so the assistant replies in the user's language while keeping equipment names and maintenance-ticket content in English.
Changes:
- Introduces
src/i18n/{config,locale,request,actions}.tsplus 12messages/<locale>.jsonfiles and wires next-intl intonext.config.tsand the root layout. - Replaces hard-coded UI strings throughout chrome, gallery, tool detail, projects, about, and chat components with
useTranslationscalls; addsLanguageSelectorand aLocaleHtmlScriptthat fixes<html lang/dir>before paint. - Forwards the active locale to
/api/chat, wherebuildSystemPromptadds an instruction to respond in that language while keeping tool/unit names and ticket content in English; adds RTL CSS overrides for the chat FAB/sheet.
Reviewed changes
Copilot reviewed 32 out of 33 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| v5/package.json, v5/package-lock.json | Add next-intl ^4.13.0 dependency. |
| v5/next.config.ts | Wrap config with createNextIntlPlugin("./src/i18n/request.ts"). |
| v5/src/i18n/config.ts | Declare 12 supported locales (code/label/englishName/dir) and helpers. |
| v5/src/i18n/locale.ts | Resolve locale from cookie → Accept-Language → default; setLocaleCookie writer. |
| v5/src/i18n/request.ts | getRequestConfig loading messages for the resolved locale. |
| v5/src/i18n/actions.ts | changeLocale server action that sets the cookie and revalidates. |
| v5/src/app/layout.tsx | Add NextIntlClientProvider, LocaleHtmlScript, and a LocalizedTree subtree. |
| v5/src/components/LocaleHtmlScript.tsx | Inline script that sets <html lang/dir> from the cookie before paint. |
| v5/src/components/LanguageSelector.tsx | Client-side <select> that calls changeLocale and refreshes the router. |
| v5/src/components/GlobalChrome.tsx | Add language selector, translate brand tagline and status strip. |
| v5/src/components/PrimaryNav.tsx, ThemeToggle.tsx | Translate nav labels and a11y strings. |
| v5/src/components/GalleryShell.tsx, GalleryFallback.tsx | Translate gallery title, filters, headers, empty/loading states. |
| v5/src/components/DetailShell.tsx | Translate detail page chrome and table headers (data stays English). |
| v5/src/components/ChatFab.tsx | Translate chat UI; send locale to /api/chat via transport. |
| v5/src/app/projects/page.tsx, about/page.tsx | Translate static page copy. |
| v5/src/app/api/chat/route.ts | Accept locale, inject a "respond in language" section into the system prompt. |
| v5/src/styles/globals.css | Styles for .lang-select, .sr-only, and RTL overrides for chat FAB/sheet. |
| v5/messages/{en,zh-CN,es,hi,ko,ar,fr,pt-BR,ru,tr,ja,he}.json | 12 message catalogs (~108 keys each). |
Files not reviewed (1)
- v5/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,74 @@ | |||
| "use server"; | |||
| const RTL_LOCALES = ["ar", "he"]; | ||
|
|
||
| const BOOTSTRAP = `(function(){try{var m=document.cookie.match(/(?:^|; )NEXT_LOCALE=([^;]+)/);if(!m)return;var l=decodeURIComponent(m[1]);if(!l)return;var rtl=${JSON.stringify( | ||
| RTL_LOCALES | ||
| )};document.documentElement.setAttribute("lang",l);document.documentElement.setAttribute("dir",rtl.indexOf(l)>-1?"rtl":"ltr");}catch(e){}})()`; |
| if (locale && locale !== "en") { | ||
| const language = languageNameForLocale(locale); | ||
| sections.push( | ||
| `## Response language\n\nRespond to the student in **${language}**, regardless of the language they write in. Translate your explanations and conversational text into ${language}. However, ALWAYS keep the following in English so MakerLab staff can read them: tool and equipment names (use the exact catalog names), unit labels (e.g. "Prusa #1"), and — critically — the \`title\` and \`description\` you pass to the \`report_issue\` tool when filing a maintenance ticket. Maintenance ticket content must be written in English even though you reply to the student in ${language}.` | ||
| ); | ||
| } |
Adds internationalization to the v5 app using next-intl in the "App Router without i18n routing" (cookie-based) configuration, so existing routes and links stay unchanged. - 12 languages: en, zh-CN, es, hi, ko, ar (RTL), fr, pt-BR, ru, tr, ja, he (RTL). English authored by hand; the other 11 are machine-translated and want a native-speaker QC pass. - Locale stored in NEXT_LOCALE cookie (default en); first visit detected from Accept-Language. - <html lang/dir> corrected before paint via an inline script; dir="rtl" for ar/he with logical-property + minimal RTL CSS so the chat FAB/sheet don't break. - <LanguageSelector> in the top nav lists each language by endonym, sets the cookie via a Server Action, and router.refresh()es so Server Components re-render in the new locale. - All UI chrome translated: nav, status strip, gallery, tool detail headings, projects/about copy, and the full chat widget. - Locale-aware AI chat: ChatFab sends the active locale; route.ts reads it and adds one system-prompt instruction to reply in the user's language while keeping tool/unit names and maintenance ticket title/description in English for staff. Notion-sourced data (tool names, descriptions) and PDF content are intentionally not translated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The effect that cleared the "Reading: …manuals…" indicator on `status === "submitted"` called setState synchronously inside an effect body, tripping react-hooks/set-state-in-effect (the only ESLint error in the repo, unrelated to i18n). Clear `readingManuals` in the send path instead — a small `send()` helper used by both `handleSuggestion` and `handleSubmit` resets the indicator right where the request starts, removing the effect entirely. A retry that re-attaches manuals still repopulates the indicator via the existing `onData` handler. `npm run lint` and `npm run typecheck` now report 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 21c2679708
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "@ai-sdk/react": "^3.0.97", | ||
| "ai": "^6.0.95", | ||
| "next": "16.1.6", | ||
| "next-intl": "^4.13.0", |
There was a problem hiding this comment.
Regenerate the lockfile for next-intl
With the new next-intl dependency, the committed lockfile is not installable: I checked cd v5 && npm ci, and npm exits with its documented clean-install requirement that package.json and package-lock be in sync, reporting Missing: @swc/helpers@ from lock file. Any CI/deploy path that uses npm ci will stop before build or tests until the lockfile is regenerated so the transitive peer resolution is recorded.
Useful? React with 👍 / 👎.
| const BOOTSTRAP = `(function(){try{var m=document.cookie.match(/(?:^|; )NEXT_LOCALE=([^;]+)/);if(!m)return;var l=decodeURIComponent(m[1]);if(!l)return;var rtl=${JSON.stringify( | ||
| RTL_LOCALES | ||
| )};document.documentElement.setAttribute("lang",l);document.documentElement.setAttribute("dir",rtl.indexOf(l)>-1?"rtl":"ltr");}catch(e){}})()`; |
There was a problem hiding this comment.
Apply header-detected locales before a cookie exists
When a first-time visitor has no NEXT_LOCALE cookie but sends Accept-Language: ar or he, resolveLocale() renders localized content from the header, but this bootstrap script immediately returns because there is no cookie. The static shell therefore remains lang="en" dir="ltr", so the RTL CSS under [dir="rtl"] and screen-reader language metadata are wrong until the user changes away/back to create a cookie.
Useful? React with 👍 / 👎.
2e712f7 to
be760b2
Compare
Summary
Adds internationalization to the v5 app with a top-nav language selector supporting 12 languages common at Cornell Tech. Built on
next-intlin the "App Router without i18n routing" variant — locale lives in a cookie (NEXT_LOCALE), so existing routes and links are unchanged (no[locale]segment).First visit detects locale from
Accept-Language, falling back toen. Locale-aware AI chat replies in the user's language while keeping equipment names and maintenance-ticket content in English for staff.What's translated
Intentionally not translated: Notion-sourced data (tool/unit names, descriptions, condition/status enum values) and PDF/manual content — these stay in English so staff and tickets remain consistent.
Languages
enEnglish ·zh-CN中文(简体)·esEspañol ·hiहिन्दी ·ko한국어 ·arالعربية (RTL) ·frFrançais ·pt-BRPortuguês (Brasil) ·ruРусский ·trTürkçe ·ja日本語 ·heעברית (RTL)How it works
src/i18n/config.ts— locale list (BCP-47 + endonym + dir + English name), helperssrc/i18n/locale.ts— cookie + Accept-Language resolution;setLocaleCookiesrc/i18n/request.ts— next-intlgetRequestConfig(cookie locale → messages)src/i18n/actions.ts—changeLocaleServer Action (sets cookie +revalidatePath("/","layout"))messages/<locale>.json— 12 files, 108 keys each (parity verified)LanguageSelectorsets the cookie via the action thenrouter.refresh()so Server Components re-render in the new localeLocaleHtmlScriptcorrects<html lang/dir>from the cookie before paint (the<html>shell is statically prerendered under Cache Components; localized content streams via Suspense)RTL handling
dir="rtl"applied forar/he. Layout uses flex/grid + logical CSS, so it mirrors automatically; added minimal overrides to flip the right-pinned chat FAB and chat sheet to the left edge (mobile breakpoint included).Notes
2e712f7):ChatFab.tsxpreviously trippedreact-hooks/set-state-in-effectby clearing the "Reading: …manuals…" indicator in an effect onstatus === "submitted". Now cleared in the send path via a smallsend()helper used by both send handlers, removing the effect entirely. This was the only ESLint error in the repo;npm run lintandnpm run typechecknow report 0 errors.route.tschange is surgical (readlocale, add one system-prompt section) to avoid conflicting with the parallel PDF-fix PR (Fix: fetch tool PDFs server-side and attach as base64 (fixes Ultimaker 400) #14, now merged tomain). Branch is off pre-Fix: fetch tool PDFs server-side and attach as base64 (fixes Ultimaker 400) #14main; the locale edit and Fix: fetch tool PDFs server-side and attach as base64 (fixes Ultimaker 400) #14's manual-attachment changes touch different parts of the file and should rebase cleanly.Test plan
npm run buildinv5/succeeds (verified: Partial Prerender for all pages)npm run lintandnpm run typecheckreport 0 errors (verified)Accept-Languagelands in that languagear/herender RTL; chat FAB + sheet sit on the left and don't break🤖 Generated with Claude Code