Add multi-language support with i18n and locale routing#5
Conversation
- Add verification meta tags for Google Search Console, Bing Webmaster Tools, and Yandex Webmaster (placeholder values to be replaced) - Expand keyword list from 14 to 29 terms for broader search coverage - Update page title and OG/Twitter titles to include target keywords - Add geo meta tags (geo.region, geo.placename, ICBM) for local SEO - Add hreflang alternate language link for en-US - Enhance robots.txt with specific directives for Googlebot, Bingbot, Yandex and their sub-crawlers, plus Host directive for Yandex - Disallow /_next/static/ and /api/ paths from crawlers - Expand sitemap from 1 to 5 entries covering all page sections - Add BreadcrumbList structured data for site navigation - Add FAQPage schema with 4 Q&A entries for rich search results https://claude.ai/code/session_01USUGkGnYqSa4L9qK9xinkE
- Add IntersectionObserver to Galaxy component to pause the requestAnimationFrame loop when the canvas is not visible in the viewport, eliminating unnecessary GPU work from two simultaneous WebGL renderers - Cache getBoundingClientRect() in Galaxy and update on resize/scroll (debounced) instead of calling it on every mouse move event - Rewrite SpotlightCard to manipulate DOM directly via refs instead of triggering React re-renders on every mouse move via setState - Gate scroll-behavior: smooth behind prefers-reduced-motion media query - Replace transition: all with specific properties (transform, box-shadow) on .btn, .work-card, and .btn-form to avoid unnecessary style recalcs https://claude.ai/code/session_01USUGkGnYqSa4L9qK9xinkE
Google is verified at the domain level via DNS in Search Console, so no HTML meta tag is needed. Bing can be imported from GSC later. Keeps only the Yandex verification meta tag. https://claude.ai/code/session_01USUGkGnYqSa4L9qK9xinkE
…tion - Change "bonuz Social Wallet" to "bonuz Lifestyle Wallet" across page and structured data - Replace "Request Intro" section with "Project Intake Request" with updated copy - Update nav button text from "Request intro" to "Project intake" https://claude.ai/code/session_01USUGkGnYqSa4L9qK9xinkE
- Create i18n infrastructure with typed dictionaries for EN, AR, DE, ZH - Add locale-based routing via [locale] dynamic segment and middleware - Extract page content into HomePage client component with dictionary props - Add stylish glassmorphism language switcher (fixed top-right corner) - Full RTL support for Arabic (mirrored layout, directional CSS) - Load locale-specific Google Fonts (Noto Sans Arabic, Noto Sans SC) - Dynamic metadata per locale (title, description, OpenGraph, hreflang) - English remains the default language (/ redirects to /en) https://claude.ai/code/session_01USUGkGnYqSa4L9qK9xinkE
- Generate sitemap entries for all 4 locales (en, ar, de, zh) with hreflang alternates for proper cross-language SEO indexing - English locale gets priority 1, others get 0.9 - Section anchors now use /en prefix for consistency - Fix not-found.tsx links to point to /en instead of bare / https://claude.ai/code/session_01USUGkGnYqSa4L9qK9xinkE
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR Review: Multi-language i18n & Locale RoutingThis is a well-structured implementation of internationalization. The architecture choices are sound and the code quality is generally high. Here are my findings: Bugs / Issues1. Locale path-switching bug in const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
const newPath = pathname.replace(new RegExp(`^/${locale}`), `/${newLocale}`);2.
// middleware.ts
import { locales, defaultLocale } from "@/lib/i18n";Note: Check if this import causes issues with Edge Runtime — if so, extract just the constants into a separate file (e.g., 3. No error handling in If a dictionary import fails (e.g., a deploy with a missing file), the error propagates uncaught. Consider wrapping with a fallback to English: export async function getDictionary(locale: Locale): Promise<Dictionary> {
return dictionaries[locale]().catch(() => dictionaries["en"]());
}Performance4. Google Fonts loaded via The original implementation used For Arabic and Chinese, SEO5. Sitemap includes URL anchors ( Fragment identifiers ( 6. Missing The alternatesLanguages["x-default"] = `${siteUrl}/en`;7. Users hitting a 404 from a non-English locale will be sent to the English home page. This is a minor UX issue — ideally the 404 page should link to the user's current locale, though this is difficult since 404 pages don't have access to the locale context. Accessibility8. The dropdown button has
Minimal fix: <button
aria-haspopup="listbox"
aria-expanded={open}
...
>
// In useEffect, add:
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
document.addEventListener("keydown", handleKeyDown);Minor / Suggestions9. No browser language detection in middleware The middleware always redirects to const acceptLanguage = request.headers.get("accept-language") ?? "";
const preferredLocale = locales.find((l) => acceptLanguage.startsWith(l)) ?? defaultLocale;This is opt-in complexity — acceptable to defer, just worth noting. 10. Array index used as {dict.whatWeDo.features.map((feature, i) => (
<li key={i}>{feature}</li>
))}Since these are static translated strings, using the feature text itself as the key would be more stable: 11. RTL arrow direction handled in CSS content, not semantically html[dir="rtl"] .features-list li::before {
content: "←";
}This is fine, but a Unicode-neutral approach like What's Done Well
Summary: The two most important issues to address before merging are the locale duplication in middleware (#2) and the path replacement bug in |
PR Review: Add multi-language support with i18n and locale routingOverall this is a solid implementation of i18n for a Next.js site. The architecture choices are sound and the code is generally clean. Here are my findings: Bugs / Correctness Issues1.
Suggestion: import from the shared module: import { locales, defaultLocale } from "@/lib/i18n";2. JSON-LD In 3. if (!locales.includes(locale as Locale)) return {};This means an invalid locale path gets served with empty metadata before the layout's 4. const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
const newPath = pathname.replace(new RegExp(`^/${locale}`), `/${newLocale}`);Performance Concerns5. Dropping The previous implementation used For the locale-specific fonts, 6. Sitemap includes URL fragment anchors — ignored by search engines { url: `${baseUrl}/en#what-we-do`, ... }
{ url: `${baseUrl}/en#our-work`, ... }Search engines universally ignore fragment identifiers in sitemaps. These entries add noise without SEO benefit. Remove them and keep only the locale root URLs. Accessibility Issues7. The button has
Minor / Code Quality8. Hard-coded Import 9. {dict.whatWeDo.features.map((feature, i) => (
<li key={i}>{feature}</li>
))}Acceptable since this list is static, but using the feature text itself as the key would be more stable. Positive Highlights
Summary
The most impactful items before merging: duplicated |
… public directory Signed-off-by: Mostafa Talaat <mostafatalaat770@gmail.com>
PR Review: Multi-language i18n SupportOverall this is a solid implementation of i18n with locale-based routing. The architecture follows Next.js 15 conventions well and the RTL support is thorough. Here are my findings: 🐛 Bugs1. Fragile locale switching in const newPath = pathname.replace(`/${locale}`, `/${newLocale}`);
const newPath = pathname.replace(new RegExp(`^/${locale}`), `/${newLocale}`);2. Redundant locale validation in The layout ( 3. Users browsing in German or Arabic who hit a 404 will be redirected to ⚡ Performance Regression4. Font loading reverts away from The original code used
The new code replaces this with a manual Consider using Also, 🔧 Code Quality5. Locale array duplicated in
// middleware.ts
import { locales, defaultLocale } from "@/lib/i18n";6. {dict.whatWeDo.features.map((feature, i) => (
<li key={i}>{feature}</li>
))}Using array index as key is fine here since the list is static, but it's worth noting this pattern suppresses the React warning while providing no reconciliation benefit. A stable key derived from content (e.g., 7. Structured data In {
"@type": "WebPage",
"@id": `${siteUrl}/#webpage`,
url: siteUrl, // Should be `${siteUrl}/${locale}`The 🔍 SEO8. Missing Google recommends including alternatesLanguages["x-default"] = `${siteUrl}/en`;9. Fragment URLs in sitemap
10. Since the root URL ( ♿ Accessibility11. Language switcher buttons lack descriptive labels The <button className="lang-option" onClick={() => switchLocale(l)}>Should be: <button
className="lang-option"
onClick={() => switchLocale(l)}
aria-label={`Switch language to ${localeNames[l]}`}
>The trigger button has ✅ What's Done Well
Summary
The locale switching path bug (#1) and font loading regression (#4) are the most important to address before merging. The middleware import duplication (#5) is a quick win that prevents future maintenance drift. |
- Add fonts.googleapis.com to style-src and fonts.gstatic.com to font-src in Content-Security-Policy so Arabic/Chinese fonts load - Import locale list from lib/i18n.ts in middleware and sitemap instead of maintaining separate copies https://claude.ai/code/session_01USUGkGnYqSa4L9qK9xinkE
PR Review: Add multi-language support with i18n and locale routingOverall, this is a solid implementation of internationalization. The architecture is clean, the RTL support is well-handled, and several nice performance improvements are bundled in. Below are observations across categories. Bugs / Correctness1. Fragile locale path replacement in LanguageSwitcher (components/LanguageSwitcher.tsx:27) String.replace() replaces only the first occurrence. If the locale code ever appears elsewhere in a URL segment (e.g., /en/posts/de-overview), this would produce an incorrect path. Use split/join on the first segment instead: 2. Hardcoded /en in app/not-found.tsx (lines 17 and 25) If the default locale ever changes, these links break silently. Import and use the defaultLocale constant from @/lib/i18n instead of hardcoding /en. 3. cachedRect initialized before canvas is attached (components/Galaxy.tsx) cachedRect is set via getBoundingClientRect() immediately, but ctn.appendChild(gl.canvas) comes a few lines later. The rect is on the container (not the canvas) so it is low-risk, but layout shifts between setup and first render could leave the cache stale until the next scroll fires the debounced update. Performance4. Google Fonts via link tags instead of next/font/google (app/[locale]/layout.tsx) The previous code used next/font/google (self-hosted, zero layout shift, automatic subsetting). The new approach loads fonts externally at runtime, which requires an extra network round-trip to Google's CDN, can cause a Flash of Unstyled Text (FOUT), and loses Next.js's automatic font size adjustment that prevents CLS. Supporting multiple font families per locale is achievable with next/font/google too. Consider whether this tradeoff is intentional, as it may impact Core Web Vitals. 5. useCallback in SpotlightCard without React.memo (components/SpotlightCard.tsx) SpotlightCard is not wrapped in React.memo, so parent re-renders still recreate all callbacks regardless of useCallback. The direct DOM manipulation via overlayRef is an excellent improvement that avoids React state churn on every mousemove, but the useCallback wrappers add overhead without measurable benefit here. Harmless, just unnecessary. Code Quality6. Array index used as React key (components/HomePage.tsx) For this static list the risk is low, but using the feature text as the key (key={feature}) is more semantically correct. 7. JSON-LD WebPage schema uses the base URL, not locale URL (app/[locale]/layout.tsx) The structuredData is a module-level constant shared across all locales, so WebPage and WebSite schema always points to https://bonuz.tech rather than https://bonuz.tech/en. The comment "kept in English for schema.org" is valid, but the url fields should ideally reflect the canonical locale URL for accurate Google attribution. 8. Dynamic lastModified in sitemap (app/sitemap.ts) Using new Date() means every sitemap request returns the current timestamp for every URL, signaling to crawlers that everything changed on every visit. Consider using fixed dates tied to actual content changes. Security9. dangerouslySetInnerHTML for JSON-LD is safe here The structured data is a hardcoded module-level constant with no user input, so there is no XSS risk. The CSP updates to allow fonts.googleapis.com in style-src and fonts.gstatic.com in font-src are also correct. What's Well Done
Summary
The two items worth addressing before merge: the fragile switchLocale path replacement and the font loading strategy (if Core Web Vitals matter). The /en hardcoding in not-found.tsx is a quick one-line fix. |
Summary
Implemented comprehensive internationalization (i18n) support for the Bonuz Technology website, enabling content delivery in English, Arabic, German, and Simplified Chinese with proper locale-based routing, RTL support, and dynamic metadata generation.
Key Changes
Internationalization Infrastructure
lib/i18n.tswith locale configuration, type definitions, and dictionary loaderlib/dictionaries/)LanguageSwitchercomponent with dropdown UI for language selectionRouting & Layout
app/[locale]/layout.tsxgenerateMetadata()function that loads locale-specific contentapp/[locale]/page.tsxfor locale-specific home page renderingComponent Updates
HomePagecomponent to acceptdictandlocaleprops for dynamic content renderingStyling & Localization
dirattribute handling.lang-switcher,.logo,.features-list)SEO & Metadata
Performance & UX
SpotlightCardcomponent withuseCallbackand intersection observer patternConfiguration Updates
robots.txtwith crawler-specific rules for Google, Bing, and Yandex/eninstead of/Notable Implementation Details
inset-inline-start,padding-inline-start) for better maintainabilityuseRouterfor seamless locale switchingzh_CNfor Chinese)https://claude.ai/code/session_01USUGkGnYqSa4L9qK9xinkE