Skip to content

Add i18n with language selector (12 languages, cookie-based)#15

Merged
philosophercode merged 2 commits into
mainfrom
philosophercode/i18n-language-selector
May 29, 2026
Merged

Add i18n with language selector (12 languages, cookie-based)#15
philosophercode merged 2 commits into
mainfrom
philosophercode/i18n-language-selector

Conversation

@philosophercode

@philosophercode philosophercode commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

Adds internationalization to the v5 app with a top-nav language selector supporting 12 languages common at Cornell Tech. Built on next-intl in 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 to en. 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

  • Top nav (TOOLS / PROJECTS / ABOUT), brand tagline, theme-toggle + language-selector aria
  • Status strip ("N TOOLS IN INVENTORY" with ICU count)
  • Gallery: title, search, CATEGORY/TRAINING filters, GRID/TABLE toggle, table headers, empty + loading states
  • Tool detail: breadcrumbs, chips, View Safety Doc / View SOP, Materials, Safety & Access, PPE Required, Emergency Stop, Use Restrictions, Documents & Resources, Details table, Physical Machines, Notes & Tips, back link
  • Projects + About page copy
  • Full chat widget: greeting, suggestion chips, composer placeholder, send/attach aria, typing/reading status, tool-running labels (Looking up unit / Filing ticket), errors, upload messages

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

en English · zh-CN 中文(简体)· es Español · hi हिन्दी · ko 한국어 · ar العربية (RTL) · fr Français · pt-BR Português (Brasil) · ru Русский · tr Türkçe · ja 日本語 · he עברית (RTL)

How it works

  • src/i18n/config.ts — locale list (BCP-47 + endonym + dir + English name), helpers
  • src/i18n/locale.ts — cookie + Accept-Language resolution; setLocaleCookie
  • src/i18n/request.ts — next-intl getRequestConfig (cookie locale → messages)
  • src/i18n/actions.tschangeLocale Server Action (sets cookie + revalidatePath("/","layout"))
  • messages/<locale>.json — 12 files, 108 keys each (parity verified)
  • LanguageSelector sets the cookie via the action then router.refresh() so Server Components re-render in the new locale
  • LocaleHtmlScript corrects <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 for ar/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

Test plan

  • npm run build in v5/ succeeds (verified: Partial Prerender for all pages)
  • npm run lint and npm run typecheck report 0 errors (verified)
  • Selector lists all 12 endonyms; choosing one persists across reloads (cookie)
  • First visit with a non-English Accept-Language lands in that language
  • ar / he render RTL; chat FAB + sheet sit on the left and don't break
  • Chat replies in the selected language; tool/unit names + filed ticket title/description stay English
  • Reading-manuals indicator clears on the next send and reappears when manuals re-attach
  • Tool detail, gallery, about, projects all show translated chrome

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 28, 2026 22:25
@vercel

vercel Bot commented May 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
makerlab-tools Error Error May 28, 2026 10:28pm
makerlab-tools-g4gb Ready Ready Preview, Comment May 28, 2026 10:28pm

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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}.ts plus 12 messages/<locale>.json files and wires next-intl into next.config.ts and the root layout.
  • Replaces hard-coded UI strings throughout chrome, gallery, tool detail, projects, about, and chat components with useTranslations calls; adds LanguageSelector and a LocaleHtmlScript that fixes <html lang/dir> before paint.
  • Forwards the active locale to /api/chat, where buildSystemPrompt adds 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.

Comment thread v5/src/i18n/locale.ts
@@ -0,0 +1,74 @@
"use server";
Comment on lines +10 to +14
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){}})()`;
Comment on lines +360 to +365
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}.`
);
}
philosophercode and others added 2 commits May 28, 2026 18:26
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread v5/package-lock.json
"@ai-sdk/react": "^3.0.97",
"ai": "^6.0.95",
"next": "16.1.6",
"next-intl": "^4.13.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +12 to +14
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){}})()`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@philosophercode philosophercode force-pushed the philosophercode/i18n-language-selector branch from 2e712f7 to be760b2 Compare May 28, 2026 22:27
@philosophercode philosophercode merged commit d18e43b into main May 29, 2026
2 of 3 checks passed
@philosophercode philosophercode deleted the philosophercode/i18n-language-selector branch May 29, 2026 05:23
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