From cf75da8f8a137f3cc9465f361d6e8dfb8be8863e Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 12 Mar 2026 05:36:58 -0700 Subject: [PATCH] Internationalize website with next-intl for 19 languages (#1216) * 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 content 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 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 --- web/app/{ => [locale]}/(legal)/eula/page.tsx | 2 +- web/app/{ => [locale]}/(legal)/layout.tsx | 0 .../(legal)/privacy-policy/page.tsx | 3 +- .../(legal)/terms-of-service/page.tsx | 0 web/app/{ => [locale]}/assets/images.d.ts | 0 .../{ => [locale]}/assets/landing-image.png | Bin web/app/[locale]/blog/cmd-shift-u/page.tsx | 74 ++ .../[locale]/blog/introducing-cmux/page.tsx | 72 ++ web/app/{ => [locale]}/blog/layout.tsx | 36 +- web/app/[locale]/blog/page.tsx | 60 ++ web/app/[locale]/blog/show-hn-launch/page.tsx | 151 ++++ .../blog/show-hn-launch/star-history.png | Bin web/app/[locale]/blog/zen-of-cmux/page.tsx | 58 ++ web/app/{ => [locale]}/community/page.tsx | 51 +- .../{ => [locale]}/components/blog-cta.tsx | 2 +- .../{ => [locale]}/components/blog-pager.tsx | 9 +- .../{ => [locale]}/components/blog-posts.ts | 4 + web/app/{ => [locale]}/components/callout.tsx | 0 .../{ => [locale]}/components/code-block.tsx | 0 web/app/[locale]/components/docs-nav-items.ts | 10 + .../{ => [locale]}/components/docs-pager.tsx | 9 +- .../components/docs-sidebar.tsx | 7 +- .../components/download-button.tsx | 4 +- .../{ => [locale]}/components/fade-image.tsx | 0 .../components/github-button.tsx | 4 +- .../components/github-stars.tsx | 0 .../[locale]/components/language-switcher.tsx | 52 ++ .../components/mobile-drawer.tsx | 0 .../{ => [locale]}/components/nav-links.tsx | 15 +- .../{ => [locale]}/components/site-footer.tsx | 84 ++- .../{ => [locale]}/components/site-header.tsx | 18 +- .../components/spacing-control.tsx | 0 web/app/{ => [locale]}/docs/api/page.tsx | 211 +++--- .../docs/browser-automation/page.tsx | 114 ++- .../docs/changelog/changelog-media.ts | 0 .../{ => [locale]}/docs/changelog/page.tsx | 19 +- web/app/[locale]/docs/concepts/page.tsx | 193 +++++ web/app/[locale]/docs/configuration/page.tsx | 136 ++++ web/app/{ => [locale]}/docs/docs-nav.tsx | 0 .../[locale]/docs/getting-started/page.tsx | 79 ++ .../[locale]/docs/keyboard-shortcuts/page.tsx | 24 + web/app/[locale]/docs/layout.tsx | 38 + .../docs/notifications/page.tsx | 161 ++-- web/app/[locale]/docs/page.tsx | 11 + web/app/[locale]/keyboard-shortcuts.tsx | 268 +++++++ web/app/[locale]/layout.tsx | 134 ++++ web/app/[locale]/page.tsx | 310 ++++++++ web/app/{ => [locale]}/posthog.tsx | 0 web/app/{ => [locale]}/providers.tsx | 0 web/app/{ => [locale]}/testimonials.tsx | 71 +- web/app/{ => [locale]}/theme.tsx | 0 web/app/{ => [locale]}/typing.tsx | 21 +- web/app/[locale]/wall-of-love/page.tsx | 43 ++ web/app/blog/cmd-shift-u/page.tsx | 82 --- web/app/blog/introducing-cmux/page.tsx | 99 --- web/app/blog/page.tsx | 32 - web/app/blog/show-hn-launch/page.tsx | 212 ------ web/app/blog/zen-of-cmux/page.tsx | 80 -- web/app/components/docs-nav-items.ts | 10 - web/app/docs/concepts/page.tsx | 212 ------ web/app/docs/configuration/page.tsx | 152 ---- web/app/docs/getting-started/page.tsx | 92 --- web/app/docs/keyboard-shortcuts/page.tsx | 20 - web/app/docs/layout.tsx | 30 - web/app/docs/page.tsx | 5 - web/app/keyboard-shortcuts.tsx | 365 --------- web/app/layout.tsx | 100 +-- web/app/page.tsx | 281 ------- web/app/sitemap.ts | 51 +- web/app/wall-of-love/page.tsx | 31 - web/bun.lock | 85 +++ web/i18n/navigation.ts | 5 + web/i18n/request.ts | 15 + web/i18n/routing.ts | 53 ++ web/messages/ar.json | 587 +++++++++++++++ web/messages/bs.json | 587 +++++++++++++++ web/messages/da.json | 587 +++++++++++++++ web/messages/de.json | 587 +++++++++++++++ web/messages/en.json | 589 +++++++++++++++ web/messages/es.json | 587 +++++++++++++++ web/messages/fr.json | 587 +++++++++++++++ web/messages/it.json | 587 +++++++++++++++ web/messages/ja.json | 587 +++++++++++++++ web/messages/km.json | 587 +++++++++++++++ web/messages/ko.json | 587 +++++++++++++++ web/messages/no.json | 587 +++++++++++++++ web/messages/pl.json | 587 +++++++++++++++ web/messages/pt-BR.json | 587 +++++++++++++++ web/messages/ru.json | 587 +++++++++++++++ web/messages/th.json | 587 +++++++++++++++ web/messages/tr.json | 587 +++++++++++++++ web/messages/zh-CN.json | 587 +++++++++++++++ web/messages/zh-TW.json | 587 +++++++++++++++ web/middleware.ts | 8 + web/next.config.ts | 5 +- web/package-lock.json | 695 +++++++++++++++++- web/package.json | 1 + 97 files changed, 14219 insertions(+), 2215 deletions(-) rename web/app/{ => [locale]}/(legal)/eula/page.tsx (99%) rename web/app/{ => [locale]}/(legal)/layout.tsx (100%) rename web/app/{ => [locale]}/(legal)/privacy-policy/page.tsx (97%) rename web/app/{ => [locale]}/(legal)/terms-of-service/page.tsx (100%) rename web/app/{ => [locale]}/assets/images.d.ts (100%) rename web/app/{ => [locale]}/assets/landing-image.png (100%) create mode 100644 web/app/[locale]/blog/cmd-shift-u/page.tsx create mode 100644 web/app/[locale]/blog/introducing-cmux/page.tsx rename web/app/{ => [locale]}/blog/layout.tsx (51%) create mode 100644 web/app/[locale]/blog/page.tsx create mode 100644 web/app/[locale]/blog/show-hn-launch/page.tsx rename web/app/{ => [locale]}/blog/show-hn-launch/star-history.png (100%) create mode 100644 web/app/[locale]/blog/zen-of-cmux/page.tsx rename web/app/{ => [locale]}/community/page.tsx (83%) rename web/app/{ => [locale]}/components/blog-cta.tsx (89%) rename web/app/{ => [locale]}/components/blog-pager.tsx (82%) rename web/app/{ => [locale]}/components/blog-posts.ts (89%) rename web/app/{ => [locale]}/components/callout.tsx (100%) rename web/app/{ => [locale]}/components/code-block.tsx (100%) create mode 100644 web/app/[locale]/components/docs-nav-items.ts rename web/app/{ => [locale]}/components/docs-pager.tsx (82%) rename web/app/{ => [locale]}/components/docs-sidebar.tsx (79%) rename web/app/{ => [locale]}/components/download-button.tsx (92%) rename web/app/{ => [locale]}/components/fade-image.tsx (100%) rename web/app/{ => [locale]}/components/github-button.tsx (92%) rename web/app/{ => [locale]}/components/github-stars.tsx (100%) create mode 100644 web/app/[locale]/components/language-switcher.tsx rename web/app/{ => [locale]}/components/mobile-drawer.tsx (100%) rename web/app/{ => [locale]}/components/nav-links.tsx (78%) rename web/app/{ => [locale]}/components/site-footer.tsx (50%) rename web/app/{ => [locale]}/components/site-header.tsx (92%) rename web/app/{ => [locale]}/components/spacing-control.tsx (100%) rename web/app/{ => [locale]}/docs/api/page.tsx (66%) rename web/app/{ => [locale]}/docs/browser-automation/page.tsx (77%) rename web/app/{ => [locale]}/docs/changelog/changelog-media.ts (100%) rename web/app/{ => [locale]}/docs/changelog/page.tsx (94%) create mode 100644 web/app/[locale]/docs/concepts/page.tsx create mode 100644 web/app/[locale]/docs/configuration/page.tsx rename web/app/{ => [locale]}/docs/docs-nav.tsx (100%) create mode 100644 web/app/[locale]/docs/getting-started/page.tsx create mode 100644 web/app/[locale]/docs/keyboard-shortcuts/page.tsx create mode 100644 web/app/[locale]/docs/layout.tsx rename web/app/{ => [locale]}/docs/notifications/page.tsx (53%) create mode 100644 web/app/[locale]/docs/page.tsx create mode 100644 web/app/[locale]/keyboard-shortcuts.tsx create mode 100644 web/app/[locale]/layout.tsx create mode 100644 web/app/[locale]/page.tsx rename web/app/{ => [locale]}/posthog.tsx (100%) rename web/app/{ => [locale]}/providers.tsx (100%) rename web/app/{ => [locale]}/testimonials.tsx (87%) rename web/app/{ => [locale]}/theme.tsx (100%) rename web/app/{ => [locale]}/typing.tsx (83%) create mode 100644 web/app/[locale]/wall-of-love/page.tsx delete mode 100644 web/app/blog/cmd-shift-u/page.tsx delete mode 100644 web/app/blog/introducing-cmux/page.tsx delete mode 100644 web/app/blog/page.tsx delete mode 100644 web/app/blog/show-hn-launch/page.tsx delete mode 100644 web/app/blog/zen-of-cmux/page.tsx delete mode 100644 web/app/components/docs-nav-items.ts delete mode 100644 web/app/docs/concepts/page.tsx delete mode 100644 web/app/docs/configuration/page.tsx delete mode 100644 web/app/docs/getting-started/page.tsx delete mode 100644 web/app/docs/keyboard-shortcuts/page.tsx delete mode 100644 web/app/docs/layout.tsx delete mode 100644 web/app/docs/page.tsx delete mode 100644 web/app/keyboard-shortcuts.tsx delete mode 100644 web/app/page.tsx delete mode 100644 web/app/wall-of-love/page.tsx create mode 100644 web/i18n/navigation.ts create mode 100644 web/i18n/request.ts create mode 100644 web/i18n/routing.ts create mode 100644 web/messages/ar.json create mode 100644 web/messages/bs.json create mode 100644 web/messages/da.json create mode 100644 web/messages/de.json create mode 100644 web/messages/en.json create mode 100644 web/messages/es.json create mode 100644 web/messages/fr.json create mode 100644 web/messages/it.json create mode 100644 web/messages/ja.json create mode 100644 web/messages/km.json create mode 100644 web/messages/ko.json create mode 100644 web/messages/no.json create mode 100644 web/messages/pl.json create mode 100644 web/messages/pt-BR.json create mode 100644 web/messages/ru.json create mode 100644 web/messages/th.json create mode 100644 web/messages/tr.json create mode 100644 web/messages/zh-CN.json create mode 100644 web/messages/zh-TW.json create mode 100644 web/middleware.ts diff --git a/web/app/(legal)/eula/page.tsx b/web/app/[locale]/(legal)/eula/page.tsx similarity index 99% rename from web/app/(legal)/eula/page.tsx rename to web/app/[locale]/(legal)/eula/page.tsx index 114f32bb..85676b2d 100644 --- a/web/app/(legal)/eula/page.tsx +++ b/web/app/[locale]/(legal)/eula/page.tsx @@ -8,7 +8,7 @@ export const metadata: Metadata = { export default function EulaPage() { return ( <> -

End-User License Agreement

+

EULA

Last updated: December 2, 2025

diff --git a/web/app/(legal)/layout.tsx b/web/app/[locale]/(legal)/layout.tsx similarity index 100% rename from web/app/(legal)/layout.tsx rename to web/app/[locale]/(legal)/layout.tsx diff --git a/web/app/(legal)/privacy-policy/page.tsx b/web/app/[locale]/(legal)/privacy-policy/page.tsx similarity index 97% rename from web/app/(legal)/privacy-policy/page.tsx rename to web/app/[locale]/(legal)/privacy-policy/page.tsx index 982b07f6..fc945d36 100644 --- a/web/app/(legal)/privacy-policy/page.tsx +++ b/web/app/[locale]/(legal)/privacy-policy/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import { Link } from "../../../../i18n/navigation"; export const metadata: Metadata = { title: "Privacy Policy — cmux", @@ -29,7 +30,7 @@ export default function PrivacyPolicyPage() {

By using our Service, you accept this Privacy Policy and our{" "} - Terms of Service, and you consent to + Terms of Service, and you consent to our collection, storage, use and disclosure of your information as described here.

diff --git a/web/app/(legal)/terms-of-service/page.tsx b/web/app/[locale]/(legal)/terms-of-service/page.tsx similarity index 100% rename from web/app/(legal)/terms-of-service/page.tsx rename to web/app/[locale]/(legal)/terms-of-service/page.tsx diff --git a/web/app/assets/images.d.ts b/web/app/[locale]/assets/images.d.ts similarity index 100% rename from web/app/assets/images.d.ts rename to web/app/[locale]/assets/images.d.ts diff --git a/web/app/assets/landing-image.png b/web/app/[locale]/assets/landing-image.png similarity index 100% rename from web/app/assets/landing-image.png rename to web/app/[locale]/assets/landing-image.png diff --git a/web/app/[locale]/blog/cmd-shift-u/page.tsx b/web/app/[locale]/blog/cmd-shift-u/page.tsx new file mode 100644 index 00000000..815d4790 --- /dev/null +++ b/web/app/[locale]/blog/cmd-shift-u/page.tsx @@ -0,0 +1,74 @@ +import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; +import { Link } from "../../../../i18n/navigation"; + +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "blog.cmdShiftU" }); + const url = locale === "en" ? "/blog/cmd-shift-u" : `/${locale}/blog/cmd-shift-u`; + return { + title: t("metaTitle"), + description: t("metaDescription"), + keywords: [ + "cmux", "terminal", "macOS", "notifications", "AI coding agents", + "keyboard shortcuts", "developer tools", "workflow", + ], + openGraph: { + title: t("metaTitle"), + description: t("metaDescription"), + type: "article", + publishedTime: "2026-03-04T00:00:00Z", + url, + }, + twitter: { + card: "summary", + title: t("metaTitle"), + description: t("metaDescription"), + }, + alternates: { canonical: url }, + }; +} + +export default function CmdShiftUPage() { + const t = useTranslations("blog.posts.cmdShiftU"); + const tc = useTranslations("common"); + + return ( + <> +
+ + ← {tc("backToBlog")} + +
+ +

{t("title")}

+ + +

{t("p1")}

+ +