* Add i18n framework with next-intl for 19 languages
Set up complete internationalization infrastructure:
- Install next-intl v4 with App Router support
- Create i18n config (routing, request, navigation)
- Add middleware for automatic locale detection from Accept-Language
- Restructure all routes under app/[locale]/
- Extract UI strings to messages/en.json
- Update all components to use useTranslations()
- Add language switcher dropdown in footer
- Support RTL for Arabic and Khmer
- Update sitemap with locale alternates
- Add generateStaticParams for all 19 locales
Languages: en, ja, zh-CN, zh-TW, ko, de, es, fr, it, da, pl, ru, bs, ar, no, pt-BR, th, tr, km
Locale detection: auto-detect from browser Accept-Language header,
with cookie persistence and locale prefix only for non-default (en).
* Add translations for de, fr, it, ja, zh-CN, zh-TW
* Add translations for ar, bs, da, es, km, no, pl, pt-BR, ru, th, tr
* Convert docs and legal pages to use useTranslations()
* Add i18n to keyboard shortcuts component
* Add i18n to wall-of-love, add missing blog posts to sitemap
* Add keyboard shortcuts and wallOfLove translations to all locales
* Update bun lockfile for next-intl dependency
* Fix t.rich() configPath: pass ReactNode not function for {var} interpolation
* Fix configPath: use rich text tag instead of plain interpolation for ReactNode
* Fix t.rich() interpolation: use rich text tags for all ReactNode placeholders
Changed {legacy}, {openShortcut}, {jumpShortcut} from plain variable
interpolation to <tag>content</tag> format so t.rich() gets proper
functions instead of values.
* Escape ICU curly braces in socketCallout rich text across all locales
* Fix i18n issues: Khmer RTL, zh-CN quality, locale-aware testimonials, hardcoded strings
- Fix Khmer (km) incorrectly marked as RTL (it's LTR, only Arabic is RTL)
- Fix zh-CN/zh-TW taglinePrefix to mention terminals and open source
- Add locale-aware testimonial translations: show original text, translate
for non-matching locales, skip translation when locale matches original
- Translate hardcoded English table content in notifications page
- Add testimonial translations to all 19 locale files
- Remove unused setRequestLocale import and params from home page
* Address PR review comments: metadata localization, blog fixes, legal pages, accessibility
- Convert hardcoded metadata to generateMetadata with getTranslations on all docs, blog, community, and wall-of-love pages
- Fix blog canonical/OG URLs to be locale-aware
- Fix introducing-cmux .split(": ") by using separate label/desc translation keys
- Revert legal page titles to English (legal content stays English-only)
- Add focus-visible ring to language switcher for keyboard accessibility
- Preserve query string and hash when switching locale
- Convert site-footer to server component (remove unnecessary "use client")
- Remove .toLowerCase() on translated text in community page
- Add /docs/browser-automation and /wall-of-love to sitemap
- Fix keyboard-shortcuts jump link visibility with trimmed query
- Deduplicate blogSlugs by importing from blog-posts.ts
- Add typingCodingAgents/typingMultitasking translation keys to all locales
- Fix Spanish accent/tilde issues in es.json testimonials
- Fix nested <a> tag in homepage keyboard shortcuts feature
- Remove unused setRequestLocale import from homepage
* Convert remaining layout/index metadata to generateMetadata
- Root layout: locale-aware title, description, OG, and Twitter card metadata
- Docs layout: translated title template
- Blog layout: translated title template
- Blog index: locale-aware metadata
* Add translated metadata keys to all locales, fix docs redirect
- Add meta.title/description/ogDescription to all 18 non-English locales
- Add docs.layoutTitle, blog.layoutTitle/metaTitle/metaDescription to all locales
- Add blog post metadata (zenOfCmux, cmdShiftU, showHnLaunch, introducingCmux) to all locales
- Add community.metaTitle/metaDescription to all locales
- Fix docs index redirect to preserve locale prefix
* Add translated docs page metaTitle keys to all locales
147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import { useTranslations } from "next-intl";
|
|
import { Link } from "../../../i18n/navigation";
|
|
import { NavLinks } from "./nav-links";
|
|
import { DownloadButton } from "./download-button";
|
|
import { ThemeToggle } from "../theme";
|
|
import { GitHubStarsBadge } from "./github-stars";
|
|
import {
|
|
useMobileDrawer,
|
|
MobileDrawerOverlay,
|
|
MobileDrawerToggle,
|
|
} from "./mobile-drawer";
|
|
|
|
export function SiteHeader({
|
|
section,
|
|
hideLogo,
|
|
}: {
|
|
section?: string;
|
|
hideLogo?: boolean;
|
|
}) {
|
|
const t = useTranslations("nav");
|
|
const tc = useTranslations("common");
|
|
const { open, toggle, close, drawerRef, buttonRef } = useMobileDrawer();
|
|
|
|
return (
|
|
<>
|
|
<header className="sticky top-0 z-30 w-full bg-background">
|
|
<div className="w-full max-w-6xl mx-auto flex items-center px-6 h-12">
|
|
{/* Left: logo + section */}
|
|
<div className="flex flex-1 items-center gap-3 min-w-0">
|
|
{!hideLogo && (
|
|
<>
|
|
<Link href="/" className="flex items-center gap-2.5">
|
|
<img
|
|
src="/logo.png"
|
|
alt="cmux"
|
|
width={24}
|
|
height={24}
|
|
className="rounded-md"
|
|
/>
|
|
<span className="text-sm font-semibold tracking-tight">
|
|
cmux
|
|
</span>
|
|
</Link>
|
|
{section && (
|
|
<>
|
|
<span className="text-border text-[13px]">/</span>
|
|
<span className="text-[13px] text-muted">{section}</span>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Center: nav links */}
|
|
<nav className="hidden md:flex items-center justify-center gap-4 text-sm text-muted shrink-0">
|
|
<NavLinks />
|
|
</nav>
|
|
|
|
{/* Right: GitHub stars + Download + theme + mobile */}
|
|
<div className="flex flex-1 items-center justify-end gap-3 min-w-0">
|
|
<GitHubStarsBadge />
|
|
<div className="hidden md:block">
|
|
<DownloadButton size="sm" location="navbar" />
|
|
</div>
|
|
<ThemeToggle />
|
|
<MobileDrawerToggle
|
|
open={open}
|
|
onClick={toggle}
|
|
buttonRef={buttonRef}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Mobile overlay + drawer */}
|
|
<MobileDrawerOverlay open={open} onClose={close} />
|
|
<nav
|
|
ref={drawerRef}
|
|
role="navigation"
|
|
aria-label="Main navigation"
|
|
className={`fixed inset-y-0 right-0 z-50 w-56 bg-background border-l border-border overflow-y-auto transition-transform md:hidden ${
|
|
open ? "translate-x-0" : "translate-x-full invisible"
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-end gap-1 px-4 h-12">
|
|
<ThemeToggle />
|
|
<button
|
|
onClick={close}
|
|
className="w-8 h-8 flex items-center justify-center text-muted hover:text-foreground transition-colors"
|
|
aria-label={tc("closeMenu")}
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<path d="M18 6L6 18M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-3 text-sm text-muted px-4 pb-4">
|
|
<Link
|
|
href="/docs/getting-started"
|
|
onClick={close}
|
|
className="hover:text-foreground transition-colors py-1"
|
|
>
|
|
{t("docs")}
|
|
</Link>
|
|
<Link
|
|
href="/blog"
|
|
onClick={close}
|
|
className="hover:text-foreground transition-colors py-1"
|
|
>
|
|
{t("blog")}
|
|
</Link>
|
|
<Link
|
|
href="/docs/changelog"
|
|
onClick={close}
|
|
className="hover:text-foreground transition-colors py-1"
|
|
>
|
|
{t("changelog")}
|
|
</Link>
|
|
<Link
|
|
href="/community"
|
|
onClick={close}
|
|
className="hover:text-foreground transition-colors py-1"
|
|
>
|
|
{t("community")}
|
|
</Link>
|
|
<GitHubStarsBadge location="mobile_drawer" />
|
|
<div className="pt-2">
|
|
<DownloadButton size="sm" location="mobile_drawer" />
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
</>
|
|
);
|
|
}
|