Skip to content

Internationalize website with next-intl for 19 languages - #1216

Merged
lawrencecchen merged 17 commits into
mainfrom
task-i18n-website
Mar 12, 2026
Merged

lawrencecchen merged 17 commits into
mainfrom
task-i18n-website

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add next-intl framework with middleware-based locale detection from Accept-Language header
  • Support 19 languages: en, ja, ko, zh-CN, zh-TW, de, fr, it, es, pt-BR, da, no, pl, ru, bs, ar, th, tr, km
  • Locale prefix as-needed (no /en/ prefix for English, other locales get /ja/, /de/, etc.)
  • RTL support for Arabic and Khmer via dir="rtl" on <html>
  • Language switcher dropdown in footer
  • All pages converted to use useTranslations(): home, blog posts, docs (8 pages), legal (3 pages), community, wall-of-love, keyboard shortcuts component
  • Sitemap with alternates.languages for international SEO
  • generateStaticParams() for SSG across all 19 locales

Testing

  • SKIP_ENV_VALIDATION=1 npx next build passes
  • Check Vercel preview for locale switching via footer dropdown
  • Verify auto-detection: set browser language to Japanese, visit cmux.dev, should redirect to /ja/
  • Verify English URLs have no prefix (e.g., cmux.dev/docs/getting-started not cmux.dev/en/docs/getting-started)

Related

  • Task: internationalize the entire website including docs

Summary by cubic

Internationalized the website with next-intl for 19 languages, adding auto locale detection, localized routing, and full-page translations. Localized all metadata (including docs pages), improved the language switcher and sitemap, and fixed i18n/accessibility issues.

  • New Features

    • Localized metadata via generateMetadata + getTranslations for root layout, docs/blog layouts (title templates), blog index, posts, community, and wall‑of‑love; docs pages now have per‑locale meta titles using new keys across all locales.
    • Added translation keys for meta titles/descriptions/OG to all locales; blog and community pages now have per‑locale metadata.
    • Locale‑aware canonical/OG URLs; sitemap adds /docs/browser-automation and /wall-of-love with per‑locale alternates.
    • Language switcher preserves query/hash and has a focus‑visible ring.
  • Bug Fixes

    • i18n: removed .toLowerCase() on translations, typed phrase keys, fixed accents, corrected RTL (Arabic only; Khmer set to LTR), fixed t.rich() placeholders and ICU escapes; legal pages stay English‑only with localized Link.
    • Routing: docs index redirect now preserves the locale prefix.
    • UI: deduplicated blog slugs, fixed nested anchor on the homepage, improved keyboard‑shortcuts jump link visibility.
    • Refactor: converted site footer to a server component.

Written for commit e640389. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Full multi-language UI with many new locale translations and a language switcher.
    • Locale-aware routing, middleware, and sitemap for correct per-language pages and links.
  • Refactor

    • Site chrome (header, footer, nav), docs, blog, and core pages converted to use translations and locale-aware layout.
    • Blog/docs pages reorganized to a locale-first structure for consistent internationalized content.

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).
@vercel

vercel Bot commented Mar 12, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 12, 2026 11:58am

@coderabbitai

coderabbitai Bot commented Mar 12, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Add full i18n support: routing, middleware, request loader, navigation helpers, locale-aware layout, language switcher, translated pages/components, 18 message bundles, sitemap updates, and migrate pages into a [locale] tree while removing legacy non-locale pages.

Changes

Cohort / File(s) Summary
I18n Core
web/i18n/routing.ts, web/i18n/request.ts, web/i18n/navigation.ts, web/middleware.ts, web/next.config.ts
Add routing/locales, request message loader, navigation wrappers (Link/usePathname/useRouter), middleware wiring, and wrap Next config with next-intl.
Layout & Locale Shell
web/app/[locale]/layout.tsx, web/app/layout.tsx, web/app/[locale]/page.tsx
Introduce locale-aware Layout (setRequestLocale, getMessages, dir/lang, JSON‑LD, static params) and move home into [locale]; simplify root layout.
Pages — Localized Additions
web/app/[locale]/blog/*, web/app/[locale]/docs/*, web/app/[locale]/community/page.tsx, web/app/[locale]/wall-of-love/page.tsx, web/app/[locale]/(legal)/*
Add/restore many blog and docs pages under [locale] with metadata and useTranslations usage; pages export metadata where applicable.
Pages — Deletions (legacy)
web/app/blog/*, web/app/docs/*, web/app/page.tsx, web/app/keyboard-shortcuts.tsx
Remove legacy non-locale blog/docs/home/keyboard-shortcuts files that were migrated into the locale tree.
Components — i18n & Navigation
web/app/[locale]/components/*, web/app/components/*, web/app/[locale]/components/blog-pager.tsx
Replace hard-coded labels with useTranslations, swap Next Link/navigation imports for i18n/navigation wrappers, and update various pagers/sidebar/footer/header to use translation keys.
Language Switcher
web/app/[locale]/components/language-switcher.tsx
Add client-side locale selector that navigates to the same path under a different locale using router + pathname.
Docs Nav Data
web/app/[locale]/components/docs-nav-items.ts
Add navItems using translation title keys plus hrefs for docs navigation.
Keyboard Shortcuts (locale)
web/app/[locale]/keyboard-shortcuts.tsx
Add localized interactive KeyboardShortcuts component under [locale] (legacy file removed).
Sitemap & Config
web/app/sitemap.ts, web/package.json, web/next.config.ts
Rebuild sitemap to include per-locale alternates; add next-intl dependency and wrap Next config.
Messages (18 locales)
web/messages/{ar,bs,da,de,en,es,fr,it,ja,km,ko,no,pl,pt-BR,ru,th,tr,zh-CN,zh-TW}.json
Add 18 full translation bundles covering common, nav, footer, home, docs, blog, legal, etc. Ensure keys referenced by components exist.
Small UI edits
web/app/[locale]/(legal)/eula/page.tsx, .../privacy-policy/page.tsx, .../terms-of-service/page.tsx, various components
Minor wiring of useTranslations for headings/labels and replacement of static strings with t(...) calls.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser
    participant MW as Middleware (next-intl)
    participant Next as Next.js Router
    participant Layout as LocaleLayout
    participant Messages as getMessages
    participant Page as Page Component

    Client->>MW: Request /path or /[locale]/path
    MW->>Next: normalize/ensure locale
    Next->>Layout: render with params.locale
    Layout->>Messages: setRequestLocale + import messages
    Messages-->>Layout: messages JSON
    Layout->>Page: provide NextIntlClientProvider + messages
    Page->>Messages: useTranslations(namespace)
    Messages-->>Page: translation function t
    Page-->>Client: Render localized HTML
    Client->>Page: LanguageSwitcher selects locale
    Page->>Next: navigate to /[newLocale]/same-path
    Next->>MW: new request (repeat)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through keys and locale strings,

Routes stitched softly under my paws,
Eighteen tongues now whisper familiar things,
Pages and links obeying gentler laws,
A rabbit cheers — translations earned applause!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding internationalization support with next-intl for 19 languages.
Description check ✅ Passed The pull request description is comprehensive and well-structured. It includes a clear summary of changes (next-intl framework with 19 languages, RTL support, language switcher), testing section with specific verification steps, and a checklist covering local testing, documentation, and code review bot requests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task-i18n-website

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds full i18n support to the cmux website using next-intl, restructuring the entire Next.js app under a [locale] dynamic segment that covers 19 languages with middleware-based Accept-Language detection, an as-needed locale prefix (English URLs stay clean), a language switcher in the footer, and sitemap hreflang alternates for SEO. The approach is architecturally sound — routing, middleware, and request config all follow next-intl's recommended patterns, and the 90-file restructure is consistent.

Key findings:

  • RTL direction bug for Khmer (km): The layout applies dir="rtl" to both Arabic and Khmer, but the Khmer script is strictly left-to-right. This will mirror the entire page layout for Khmer users.
  • setRequestLocale not called in Home page: setRequestLocale is imported and params is accepted in Home, but neither is used. Per next-intl's static rendering guide, each page should call setRequestLocale(locale) — all other pages in this PR also omit this call, relying entirely on the parent layout. While the build passes, this diverges from the documented pattern and can cause locale fallback to en in certain SSG edge cases.
  • The sitemap, language switcher, routing config, and middleware are all implemented correctly.

Confidence Score: 3/5

  • Safe to merge after fixing the Khmer RTL bug — it will actively break the layout for km locale users.
  • The overall i18n architecture is correct and the build passes. Two issues prevent a higher score: the Khmer RTL bug is a confirmed rendering defect that will visually break the km locale, and the missing setRequestLocale calls in page components (not just layouts) deviate from next-intl's static rendering requirements, risking locale fallback in SSG builds.
  • web/app/[locale]/layout.tsx (RTL direction logic) and web/app/[locale]/page.tsx (unused params / missing setRequestLocale)

Important Files Changed

Filename Overview
web/app/[locale]/layout.tsx Core locale layout — correctly calls setRequestLocale and wraps with NextIntlClientProvider, but incorrectly applies dir="rtl" to Khmer (km), which is an LTR language.
web/app/[locale]/page.tsx Home page accepts params and imports setRequestLocale but never calls it — the locale is only set by the parent layout, which may cause issues in static rendering edge cases.
web/i18n/routing.ts Routing config defining 19 locales, localePrefix as-needed, and locale display names — looks correct.
web/middleware.ts Minimal next-intl middleware wiring with a standard matcher that excludes API routes, static files, and Vercel internals — correct.
web/app/sitemap.ts Generates hreflang alternates for all 19 locales per URL path, correctly omitting the /en/ prefix for English canonical URLs.
web/app/[locale]/components/language-switcher.tsx Client-side locale switcher using next-intl router.replace — straightforward and correct, preserves current pathname on locale change.
web/app/[locale]/components/site-footer.tsx Footer now uses next-intl Link and useTranslations, embeds LanguageSwitcher — translation-aware and correctly differentiates internal vs external links.
web/i18n/request.ts Request config falls back to defaultLocale for unknown locales and dynamically imports the correct messages JSON — correct pattern.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming Request] --> B{next-intl Middleware}
    B -->|Accept-Language: ja| C[Redirect to /ja/...]
    B -->|Accept-Language: en| D[No prefix — /...]
    B -->|Unknown locale| E[Fallback to /en default]

    C --> F["[locale] Layout\n(async)\n• validate locale\n• setRequestLocale\n• load messages\n• set dir= ltr/rtl"]
    D --> F
    E --> F

    F --> G{locale === 'ar'?}
    G -->|yes| H["dir='rtl'"]
    G -->|no| I["dir='ltr'"]
    H --> J[NextIntlClientProvider\nwraps children]
    I --> J

    J --> K[Page Component\nuseTranslations]
    J --> L[SiteFooter\nLanguageSwitcher]

    style G fill:#f96,stroke:#c00
    style H fill:#f96,stroke:#c00
Loading

Last reviewed commit: 79eacda

Comment thread web/app/[locale]/layout.tsx Outdated

const messages = await getMessages();

const dir = locale === "ar" || locale === "km" ? "rtl" : "ltr";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Khmer (km) is not an RTL language

The Khmer script is written left-to-right — it is not a right-to-left language. Setting dir="rtl" for km will mirror the entire page layout for Khmer users, causing text, navigation, and UI elements to render incorrectly.

Only Arabic (ar) uses RTL among the 19 supported locales.

Suggested change
const dir = locale === "ar" || locale === "km" ? "rtl" : "ltr";
const dir = locale === "ar" ? "rtl" : "ltr";

Comment thread web/app/[locale]/page.tsx Outdated
Comment on lines +13 to +18
export default function Home({
params,
}: {
params: Promise<{ locale: string }>;
}) {
return <HomeContent />;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

params accepted but never used; setRequestLocale imported but never called

setRequestLocale is imported at line 2 and the component signature accepts params, but neither is ever used. The next-intl documentation recommends calling setRequestLocale(locale) in every page component (in addition to layouts) to correctly support Static Rendering — without it, the page's own rendering context is not initialised with the locale, and pages can fall back to the defaultLocale in edge-case SSG scenarios.

Suggested change
export default function Home({
params,
}: {
params: Promise<{ locale: string }>;
}) {
return <HomeContent />;
export default async function Home({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
return <HomeContent />;
}

@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: 79eacda2e6

ℹ️ 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 web/app/[locale]/layout.tsx Outdated

const messages = await getMessages();

const dir = locale === "ar" || locale === "km" ? "rtl" : "ltr";

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 Restrict RTL direction to Arabic locale

This condition marks Khmer (km) pages as dir="rtl", but Khmer is a left-to-right script. For /km/* routes this flips document flow, alignment, and punctuation ordering across the entire page, making the localized site hard to read and navigate for Khmer users.

Useful? React with 👍 / 👎.

@cubic-dev-ai cubic-dev-ai 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.

18 issues found across 90 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/app/[locale]/blog/zen-of-cmux/page.tsx">

<violation number="1" location="web/app/[locale]/blog/zen-of-cmux/page.tsx:19">
P2: Canonical/Open Graph URL is hard-coded to the English path in a locale route, so non-English pages publish incorrect metadata URLs.</violation>
</file>

<file name="web/app/[locale]/blog/page.tsx">

<violation number="1" location="web/app/[locale]/blog/page.tsx:17">
P2: `slugToPath` is typed too loosely. Use the `blogSlugs` union as map keys so missing slug mappings are caught at compile time.</violation>
</file>

<file name="web/app/[locale]/page.tsx">

<violation number="1" location="web/app/[locale]/page.tsx:13">
P2: The page ignores `params.locale` and never calls `setRequestLocale` before using translations, which can force this route into dynamic rendering instead of static i18n output.</violation>

<violation number="2" location="web/app/[locale]/page.tsx:110">
P2: This renders a nested `<a>` inside another `<a>`, which is invalid HTML and can break accessibility/click behavior.</violation>
</file>

<file name="web/app/[locale]/blog/cmd-shift-u/page.tsx">

<violation number="1" location="web/app/[locale]/blog/cmd-shift-u/page.tsx:27">
P2: Canonical metadata is hardcoded to the English URL in a locale route, so translated pages will point search engines to the wrong canonical.</violation>
</file>

<file name="web/app/[locale]/docs/api/page.tsx">

<violation number="1" location="web/app/[locale]/docs/api/page.tsx:36">
P2: The page body is localized, but metadata remains hardcoded in English. Use locale-aware metadata generation so non-English routes don't ship English SEO title/description.</violation>
</file>

<file name="web/app/[locale]/(legal)/eula/page.tsx">

<violation number="1" location="web/app/[locale]/(legal)/eula/page.tsx:13">
P2: Only the page title is localized; the EULA body remains hardcoded in English, so non-English locales won’t get a translated legal page.</violation>
</file>

<file name="web/app/[locale]/layout.tsx">

<violation number="1" location="web/app/[locale]/layout.tsx:78">
P2: `km` (Khmer) should not be treated as RTL. This condition forces Khmer pages to render with `dir="rtl"`, which breaks layout/text direction for that locale.</violation>
</file>

<file name="web/app/[locale]/community/page.tsx">

<violation number="1" location="web/app/[locale]/community/page.tsx:49">
P2: Do not force lowercase on translated UI text; pass the localized title directly to avoid locale-specific casing issues.</violation>
</file>

<file name="web/app/[locale]/components/docs-nav-items.ts">

<violation number="1" location="web/app/[locale]/components/docs-nav-items.ts:7">
P2: `/docs/browser-automation` is added to docs navigation but missing from sitemap paths, so this page (and its locale alternates) won't be emitted in the sitemap.</violation>
</file>

<file name="web/app/[locale]/docs/getting-started/page.tsx">

<violation number="1" location="web/app/[locale]/docs/getting-started/page.tsx:7">
P2: Metadata is hardcoded in English instead of being locale-aware, so translated routes still render English SEO/title tags.</violation>
</file>

<file name="web/app/[locale]/blog/show-hn-launch/page.tsx">

<violation number="1" location="web/app/[locale]/blog/show-hn-launch/page.tsx:8">
P2: Locale-specific routes are using hard-coded English metadata and canonical URL, so non-English pages publish incorrect SEO metadata (including canonical/OG URL) instead of locale-specific values.</violation>
</file>

<file name="web/app/[locale]/blog/introducing-cmux/page.tsx">

<violation number="1" location="web/app/[locale]/blog/introducing-cmux/page.tsx:19">
P2: Canonical and Open Graph URL are fixed to the English route in a `[locale]` page, so localized pages emit the wrong canonical/share URL.</violation>

<violation number="2" location="web/app/[locale]/blog/introducing-cmux/page.tsx:55">
P2: Avoid parsing translated strings with `.split(": ")` and hard-coded English labels in localized content; this breaks for non-English punctuation/order and can render mixed-language text.</violation>
</file>

<file name="web/app/[locale]/keyboard-shortcuts.tsx">

<violation number="1" location="web/app/[locale]/keyboard-shortcuts.tsx:213">
P3: Use the normalized query for jump-link visibility; whitespace-only input currently hides jump links while showing unfiltered results.</violation>
</file>

<file name="web/app/[locale]/components/site-footer.tsx">

<violation number="1" location="web/app/[locale]/components/site-footer.tsx:1">
P2: `"use client"` is unnecessary here and forces the whole footer to hydrate on the client. Remove it so only `LanguageSwitcher` remains client-side.</violation>
</file>

<file name="web/messages/en.json">

<violation number="1" location="web/messages/en.json:60">
P2: `keyboardShortcutsDesc` includes a colon and `<link>` wrapper even though the caller already adds both, causing `: :` text and nested `<a>` elements on the home page.</violation>
</file>

<file name="web/app/[locale]/components/blog-pager.tsx">

<violation number="1" location="web/app/[locale]/components/blog-pager.tsx:6">
P2: `blogSlugs` introduces a duplicate blog source-of-truth and leaves `blog-posts.ts` unused, which makes future blog updates easy to miss in one place.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread web/app/[locale]/blog/zen-of-cmux/page.tsx Outdated
"introducingCmux",
] as const;

const slugToPath: Record<string, string> = {

@cubic-dev-ai cubic-dev-ai Bot Mar 12, 2026 •

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: slugToPath is typed too loosely. Use the blogSlugs union as map keys so missing slug mappings are caught at compile time.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/[locale]/blog/page.tsx, line 17:

<comment>`slugToPath` is typed too loosely. Use the `blogSlugs` union as map keys so missing slug mappings are caught at compile time.</comment>

<file context>
@@ -0,0 +1,52 @@
+  "introducingCmux",
+] as const;
+
+const slugToPath: Record<string, string> = {
+  cmdShiftU: "cmd-shift-u",
+  zenOfCmux: "zen-of-cmux",
</file context>
Suggested change
const slugToPath: Record<string, string> = {
const slugToPath: Record<(typeof blogSlugs)[number], string> = {
Fix with Cubic

Comment thread web/app/[locale]/page.tsx Outdated
Comment thread web/app/[locale]/page.tsx Outdated
Comment thread web/app/[locale]/blog/cmd-shift-u/page.tsx Outdated
Comment thread web/app/[locale]/blog/introducing-cmux/page.tsx Outdated
Comment thread web/app/[locale]/components/site-footer.tsx Outdated
Comment thread web/messages/en.json
Comment thread web/app/[locale]/components/blog-pager.tsx Outdated
Comment thread web/app/[locale]/keyboard-shortcuts.tsx Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
web/app/[locale]/wall-of-love/page.tsx (1)

6-10: ⚠️ Potential issue | 🟠 Major

Localize metadata along with the visible copy.

The page body now uses wallOfLove translations, but metadata.title and metadata.description are still hard-coded English. Every localized route will still show English tab text and meta description, which undercuts the international SEO work in this PR.

Also applies to: 13-23

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/wall-of-love/page.tsx around lines 6 - 10, metadata
currently contains hard-coded English title/description; replace the static
export const metadata with a dynamic generator that returns localized metadata
using the same wallOfLove translations used in the page body. Implement an
export async function generateMetadata({ params }) that loads the locale
dictionary or translation helper (the same source used by wallOfLove in the
page) and returns a Metadata object with title and description drawn from the
wallOfLove keys; keep the Metadata type and ensure pages for all locales
(including the code currently at lines 13-23) use the localized strings instead
of literals.
web/app/[locale]/community/page.tsx (1)

71-81: ⚠️ Potential issue | 🟡 Minor

Inconsistent: GitHub name is hardcoded while other link names use translations.

Line 73 uses name="GitHub" directly while Discord (line 61), Twitter (line 85), YouTube (line 97), and LinkedIn (line 109) all use t("...") for the name prop. For consistency, this should also use a translation key.

🐛 Proposed fix
          <CommunityLink
            href="https://github.com/manaflow-ai/cmux"
-           name="GitHub"
+           name={t("github")}
            action={t("githubAction")}
            description={t("githubDesc")}

Note: Ensure the translation files include a "github": "GitHub" key in the community namespace (it exists in pt-BR.json at line 83, but verify it's present in all locale files).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/community/page.tsx around lines 71 - 81, The GitHub
CommunityLink is hardcoded as name="GitHub"; change it to use the i18n helper
(name={t("github")}) to match other links (CommunityLink usage with t for
"discord", "twitter", etc.), and ensure the "github" key exists in the community
translation namespace for all locales.
web/app/[locale]/docs/notifications/page.tsx (1)

46-67: ⚠️ Potential issue | 🟡 Minor

These tables are still only partially localized.

Visible copy like Variable, Description, Notification title ..., Title + body, Yes/No, and Higher/Lower is still hard-coded in English. That leaves the page half-translated on non-English locales. Move the human-readable headers/cells into translation keys; only the env var names and protocol identifiers need to stay literal.

Also applies to: 107-136

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/docs/notifications/page.tsx around lines 46 - 67, The table
contains hard-coded English strings ("Variable", "Description", "Notification
title (workspace name or app name)", "Notification subtitle", "Notification body
text", and other human-readable cells) causing partial localization; replace
these literal strings with translation keys and call the repository's
translation helper (the same i18n/translator used elsewhere in this page) for
each header and cell while keeping the env var names (CMUX_NOTIFICATION_TITLE,
CMUX_NOTIFICATION_SUBTITLE, CMUX_NOTIFICATION_BODY) and any protocol identifiers
literal; apply the same change to the other table range mentioned (lines around
the second table at 107-136) so all visible copy is pulled from translations
rather than hard-coded English.
🧹 Nitpick comments (7)
web/app/sitemap.ts (1)

7-22: Consider using static dates or build-time constants instead of new Date().

Using new Date() for lastModified means the value changes on every build, which:

  • Creates non-deterministic builds (different output each time)
  • Signals to search engines that pages changed even when content is unchanged
  • May trigger unnecessary re-crawling

For pages with infrequent changes (docs, community), consider using a static date string or a build-time constant that only updates when content actually changes.

♻️ Suggested approach
  const paths = [
-   { path: "", lastModified: new Date(), changeFrequency: "weekly" as const, priority: 1 },
-   { path: "/blog", lastModified: new Date(), changeFrequency: "weekly" as const, priority: 0.8 },
+   { path: "", lastModified: "2026-03-12", changeFrequency: "weekly" as const, priority: 1 },
+   { path: "/blog", lastModified: "2026-03-12", changeFrequency: "weekly" as const, priority: 0.8 },
    { path: "/blog/show-hn-launch", lastModified: "2026-02-21", changeFrequency: "monthly" as const, priority: 0.7 },
    // ... similar for other entries with new Date()
  ];

Alternatively, use a build-time constant:

const BUILD_DATE = process.env.BUILD_DATE || "2026-03-12";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/sitemap.ts` around lines 7 - 22, The paths array currently sets
lastModified using new Date(), which makes builds non-deterministic; update the
lastModified entries in the paths array (and any other occurrences) to use a
static build-time value (e.g., a BUILD_DATE constant sourced from
process.env.BUILD_DATE or a fixed ISO date string) so only pages that actually
change get a new date; locate the paths constant in sitemap.ts and replace new
Date() uses with the BUILD_DATE constant (or explicit static date strings) and
ensure BUILD_DATE is defined and used consistently.
web/app/[locale]/docs/keyboard-shortcuts/page.tsx (1)

5-9: Static metadata not internationalized.

The page content uses translations via t("title") and t("description"), but the metadata export remains hardcoded in English. For consistent i18n, consider using generateMetadata to return localized metadata.

♻️ Suggested approach using generateMetadata
-export const metadata: Metadata = {
-  title: "Keyboard Shortcuts",
-  description:
-    "All cmux keyboard shortcuts for workspaces, surfaces, split panes, browser, notifications, find, and window management on macOS.",
-};
+import { getTranslations } from "next-intl/server";
+
+export async function generateMetadata(): Promise<Metadata> {
+  const t = await getTranslations("docs.keyboardShortcuts");
+  return {
+    title: t("title"),
+    description: t("metaDescription"),
+  };
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/docs/keyboard-shortcuts/page.tsx around lines 5 - 9, The
exported constant metadata is hardcoded in English; replace it with a localized
metadata provider by implementing generateMetadata to return title/description
using the existing t(...) translation function instead of the static export;
update/remove the export const metadata and add an async function
generateMetadata({ params, locale }) that calls your i18n t("title") and
t("description") (or the same translation hook used in the page) and returns the
localized Metadata object so metadata matches the page translations.
web/app/[locale]/docs/api/page.tsx (1)

6-10: Static metadata not internationalized.

Same issue as the keyboard-shortcuts page: static metadata export while page content uses translations. Consider using generateMetadata for localized SEO.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/docs/api/page.tsx around lines 6 - 10, The exported static
constant metadata is not localized; replace the static export (export const
metadata) with an async generateMetadata function that uses your
i18n/translation helper to produce title and description per locale (matching
how the page content is localized), e.g., implement generateMetadata({ params })
to read params.locale and return a Metadata object with translated
title/description so SEO/meta tags match localized content; update any
imports/types if needed to support async generateMetadata and remove the static
metadata export.
web/app/[locale]/blog/page.tsx (1)

5-8: Metadata is hardcoded in English while page content is translated.

The title and description in metadata remain static English strings, which is inconsistent with the internationalized page content. For full i18n support, consider using generateMetadata to return locale-specific metadata.

♻️ Suggested approach using generateMetadata
-export const metadata: Metadata = {
-  title: "Blog",
-  description: "News and updates from the cmux team",
-};
+import { getTranslations } from "next-intl/server";
+
+export async function generateMetadata({
+  params: { locale },
+}: {
+  params: { locale: string };
+}): Promise<Metadata> {
+  const t = await getTranslations({ locale, namespace: "blog" });
+  return {
+    title: t("title"),
+    description: t("description"),
+  };
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/blog/page.tsx around lines 5 - 8, The metadata object is
hardcoded in English; replace the static export const metadata with an exported
generateMetadata function that returns locale-specific Metadata (e.g., export
async function generateMetadata({ params }) ...) and build title/description
using the page's i18n lookup (use params.locale or your translation helper) so
metadata.title and metadata.description are localized; update any imports or
helpers used by the page component (the existing metadata symbol should be
removed/replaced and generateMetadata should return the localized
title/description).
web/app/[locale]/components/blog-pager.tsx (1)

6-11: Consider extracting shared blog slug data to avoid duplication.

The blogSlugs array here duplicates similar data in web/app/[locale]/blog/page.tsx (which has separate blogSlugs and slugToPath objects). Consider consolidating into a shared module (e.g., blog-posts.ts) to ensure they stay in sync when adding or reordering posts.

♻️ Suggested shared module
// web/app/[locale]/components/blog-data.ts
export const blogPosts = [
  { slug: "cmd-shift-u", key: "cmdShiftU" },
  { slug: "zen-of-cmux", key: "zenOfCmux" },
  { slug: "show-hn-launch", key: "showHnLaunch" },
  { slug: "introducing-cmux", key: "introducingCmux" },
] as const;

Then import this in both blog-pager.tsx and blog/page.tsx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/components/blog-pager.tsx around lines 6 - 11, Extract the
duplicated blog slugs into a single shared constant (e.g., export const
blogPosts = [... ] as const) and replace local `blogSlugs` usages with an import
of that shared constant in both the `blog-pager` component and the `blog/page`
code; also derive the `slugToPath` mapping from this shared `blogPosts` (instead
of duplicating it) so `blogSlugs` and `slugToPath` remain in sync—preserve the
`as const` typing and update any references to `blogSlugs`/`slugToPath` to use
the new exported symbol.
web/app/[locale]/components/site-footer.tsx (1)

84-89: Consider memoizing the year value.

new Date().getFullYear() is computed on every render. While negligible in most cases, you could define year as a module-level constant since it won't change during a session.

♻️ Optional refactor
+"use client";
+
+import { useTranslations } from "next-intl";
+import { Link } from "../../../i18n/navigation";
+import { LanguageSwitcher } from "./language-switcher";
+
+const CURRENT_YEAR = new Date().getFullYear();
+
 function isExternal(href: string) {
   return href.startsWith("http") || href.startsWith("mailto:");
 }

 export function SiteFooter() {
   const t = useTranslations("footer");
-  const year = new Date().getFullYear();
+  const year = CURRENT_YEAR;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/components/site-footer.tsx around lines 84 - 89, Move the
dynamic year computation out of the component render and memoize it as a
module-level constant (e.g., const year = new Date().getFullYear()) so new
Date().getFullYear() is not called on every render; update the usage in
site-footer.tsx where {t("copyright", { year })} is referenced and leave
LanguageSwitcher and the component unchanged.
web/app/[locale]/docs/concepts/page.tsx (1)

5-9: Static metadata won't be translated across locales.

The metadata export uses hardcoded English strings. For full i18n support, use generateMetadata with getTranslations to provide locale-specific title and description. This pattern is supported by next-intl and allows metadata to adapt to each locale. Other pages in the PR also have this pattern, so this may be intentional for SEO consistency.

♻️ Example using generateMetadata for locale-aware metadata
import { getTranslations } from "next-intl/server";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: "docs.concepts" });
  return {
    title: t("title"),
    description: t("metaDescription"),
  };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/docs/concepts/page.tsx around lines 5 - 9, Replace the
static metadata export with a locale-aware generateMetadata function: remove or
replace the exported const metadata and implement an exported async function
generateMetadata({ params }) that awaits params.locale, calls getTranslations
with the locale and the "docs.concepts" namespace (or appropriate namespace),
and returns title and description from the translation keys (e.g., "title" and
"metaDescription"); ensure you import getTranslations from "next-intl/server"
and keep the returned type as Promise<Metadata> so metadata is generated
per-locale.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/app/`[locale]/(legal)/eula/page.tsx:
- Around line 10-13: The page currently only localizes the EULA title via
useTranslations("legal") and t("eula") while the EULA body remains hard-coded
English; either remove the translation call for the title and render the title
as plain English to match the English-only body, or (preferred) add translation
keys for the entire EULA body in the "legal" namespace and replace the
hard-coded body with t(...) calls (or a single t("eula_full") that returns the
full localized text) so that useTranslations("legal") and t are used
consistently for both the title and the full document.

In `@web/app/`[locale]/(legal)/privacy-policy/page.tsx:
- Around line 11-15: The page currently uses useTranslations("legal") for the
heading but leaves the body copy and the inline "Terms of Service" link text
hard-coded English; update the component in page.tsx to replace the hard-coded
paragraph and the link text with translation lookups using the t function (e.g.,
t("privacyPolicyLastUpdated"), t("privacyPolicyBody"), and
t("termsOfServiceLinkText") or similar keys) and ensure those keys are added to
the locale resource files for all supported locales so the full body and link
are localized; keep the existing structure and link href but use t(...) for all
user-visible strings in the component (references: useTranslations, t in
page.tsx).

In `@web/app/`[locale]/(legal)/terms-of-service/page.tsx:
- Around line 10-13: The page uses useTranslations("legal") only for the <h1>
(t("termsOfService")) while the Terms of Service body remains hard-coded
English; update the component to source the full TOS content from your
localization system instead of inline English — either replace the hard-coded
text with translation keys accessed via useTranslations (e.g., t("termsBody", {
/* or t("termsParagraph1")... */ })) or load a locale-specific markdown/JSON
file and render it; ensure all text currently in the body is mapped to
translation keys or locale assets and rendered via the same translated-source
(symbols to change: useTranslations, t, and the page component rendering the
body).

In `@web/app/`[locale]/blog/introducing-cmux/page.tsx:
- Around line 53-60: The code is slicing translated strings with t(...).split(':
') (used for featureVerticalTabs, featureNotifications, featureSplitPanes,
featureSocketApi, featureGpu) which breaks non‑English locales; stop parsing
translations. Instead add separate translation keys for label and body (e.g.
featureVerticalTabs_label and featureVerticalTabs_text) or a single rich key
that contains the full <li> (e.g. featureVerticalTabs_full), then render using
t('featureVerticalTabs_label') + t('featureVerticalTabs_text') (or
dangerouslySetInnerHTML/Trans for rich HTML) and replace the split usage in the
list under the featuresTitle header so labels are not hard‑coded in English.
- Around line 5-27: Replace the static export const metadata with a
generateMetadata function that builds metadata per-locale: use the route param
locale passed into generateMetadata and call your i18n navigation helper
getPathname to produce the locale-aware path, then set openGraph.url and
alternates.canonical from that generated full URL; update any mentions of
title/description/keywords to reuse the existing values but ensure openGraph.url
and alternates.canonical are computed dynamically inside generateMetadata so
each locale returns the correct prefixed URL.

In `@web/app/`[locale]/blog/show-hn-launch/page.tsx:
- Around line 8-31: The metadata object currently hardcodes openGraph.url and
alternates.canonical to the English root, which breaks locale-aware canonical
URLs; replace the static export const metadata with an exported async
generateMetadata function that reads params.locale (or awaits params) and builds
baseUrl depending on whether locale === 'en' (no prefix) or includes
`/${locale}`, then return the metadata object with openGraph.url and
alternates.canonical constructed from that baseUrl (keep other fields the same);
update references to openGraph.url and alternates.canonical (and any similar
hardcoded URLs in this file) to use the generated values so non-default locales
produce locale-prefixed canonical and OG URLs.

In `@web/app/`[locale]/components/language-switcher.tsx:
- Around line 35-40: The select element in LanguageSwitcher
(language-switcher.tsx) removes the native focus indicator via the className
containing "focus:outline-none"; restore an accessible visible focus style by
removing that token and adding an explicit focus-visible style (e.g.
"focus-visible:outline" or "focus-visible:ring-2 focus-visible:ring-primary/75"
or similar) so keyboard users see a clear focus ring when using the select;
update the className on the <select> in the LanguageSwitcher component
accordingly.
- Around line 12-15: The locale switcher currently calls
router.replace(pathname) which drops the current query string and hash; update
the onChange handler to preserve them by appending the current URL suffix (query
+ hash) when navigating. Locate the onChange function and build the target URL
using either window.location.search and window.location.hash or derive the
suffix from router.asPath (e.g., router.asPath relative to pathname), then call
router.replace(targetUrl, undefined, { locale: newLocale }) so the query and
fragment are retained; keep references to onChange, router.replace, pathname,
and router.asPath/window.location in your change.

In `@web/app/`[locale]/docs/changelog/page.tsx:
- Around line 242-249: Replace the static export const metadata with a
server-side generateMetadata function that calls getTranslations from
next-intl/server using the incoming locale and returns title and description
using t("docs.changelog.title") and t("docs.changelog.metaDescription"); update
the page to accept locale params where needed. Modify formatDate to accept a
locale argument (instead of hardcoding "en-US") and use new
Intl.DateTimeFormat(locale, ...) inside formatDate; then call formatDate(locale,
date) wherever dates are rendered. Ensure imports include getTranslations and
adjust any uses of export const metadata and formatDate to the new
generateMetadata and locale-aware signature.

In `@web/app/`[locale]/keyboard-shortcuts.tsx:
- Around line 19-107: The ShortcutRow rendering is not passing the optional note
prop so scoped duplicates (e.g., shortcuts with id "ws-rename" and "wn-reload"
both using "⌘⇧R", and "br-open" and "nt-flash" both using "⌘⇧L") appear as
global conflicts; update the component usage where ShortcutRow is rendered (the
place that currently calls ShortcutRow without passing note) to include
note={s.note} (or the equivalent prop from the Shortcut object) so ShortcutRow
can display scoping notes; reference the Shortcut type's optional note field and
the variable s (shortcut) when adding the prop.
- Around line 109-110: The normalize function currently uses toLowerCase(),
which is not locale-aware; update normalize to perform locale-sensitive case
folding by calling toLocaleLowerCase with the active locale (e.g., change
signature to accept a locale param like normalize(s: string, locale: string) or
obtain the current locale from the enclosing scope) and use
s.toLocaleLowerCase(locale).keep the existing whitespace normalization
(.replace(/\s+/g, " ").trim()) unchanged so filtering still works but now
respects locale-specific casing (refer to the normalize function name when
making the change).

In `@web/app/`[locale]/layout.tsx:
- Line 78: The dir calculation currently treats locale "km" as RTL; update the
conditional that sets the dir variable so only Arabic ("ar") is considered RTL
(e.g., const dir = locale === "ar" ? "rtl" : "ltr";), leaving "km" (Khmer) and
all other locales as left-to-right; modify the expression or branching that
assigns dir (the dir variable and its locale check) to remove "km" from RTL
handling.

In `@web/app/`[locale]/page.tsx:
- Around line 108-121: Replace plain anchor elements used for internal docs (the
outer <a href="/docs/keyboard-shortcuts" className={linkClass}> and the nested
anchor returned inside t.rich(...) as well as other plain anchors pointing to
/docs/notifications and /docs/keyboard-shortcuts) with next-intl's locale-aware
Link component so client-side locale is preserved; remove the nested <a> (return
a fragment or a non-interactive span inside t.rich) and pass className to Link
(or to the inner element when using t.rich) so you avoid nested anchors and keep
styling via linkClass, updating the occurrences referenced by linkClass and
t.rich("feature.keyboardShortcutsDesc", ...) and the similar
notification/keyboard shortcut link usages.

In `@web/app/layout.tsx`:
- Around line 1-10: RootLayout currently returns only children; move the
document shell (<html> and <body>) into the RootLayout component so it renders
the top-level HTML document and wraps children, and remove those tags from the
nested locale layout (app/[locale]/layout.tsx). Specifically, update RootLayout
to render an <html> element (with appropriate lang/dir attributes if available)
containing a <body> that wraps children (and keep any global providers/metadata
here), then edit the nested layout (the file that currently renders
<html>/<body> and provides i18n) to remove the <html> and <body> elements so it
only provides locale-specific providers/wrappers (e.g., i18n provider) and
returns its children.

In `@web/i18n/routing.ts`:
- Around line 3-23: The Khmer locale "km" was incorrectly treated as RTL; keep
"km" in the locales constant if desired but remove it from any RTL handling —
locate the locales array (export const locales) and any RTL predicate/collection
(e.g., rtlLocales, isRtl, directionCheck) and ensure the RTL branch only
contains/compares to "ar" (Arabic) rather than "km"; update the RTL check to
explicitly use ['ar'] or locale === 'ar' and run the provided search to confirm
no remaining occurrences of "km" in RTL logic.

In `@web/messages/da.json`:
- Line 107: Two heading strings in the Danish locale are still English: replace
the values "The Zen of cmux" and "Wall of Love" in the da.json entries so they
are translated into Danish; locate the JSON objects where "title": "The Zen of
cmux" and "title": "Wall of Love" appear and update their "title" values to the
correct Danish translations (e.g., "Zen af cmux" or a preferred Danish phrasing,
and "Kærlighedens mur" or preferred localized phrasing) ensuring the quotes and
JSON formatting remain valid.
- Around line 203-233: The Danish file conflates "pane" and "panel" (making both
render as "Panel"); update the affected keys so the two concepts remain distinct
— specifically change paneTitle, paneDesc, paneNote, paneIdSocket to use a
distinct Danish term for "pane" (e.g., "Opdelingspanel" or "Pane" with
clarifying wording) while keeping panelTitle, panelDesc, panelNote,
panelTerminal, panelBrowser, panelIdInternal as the separate "panel" term;
ensure descriptions (paneDesc vs panelDesc) and the ID labels (paneIdSocket vs
panelIdInternal) clearly reflect their different hierarchy levels without
altering other keys.

In `@web/messages/km.json`:
- Around line 1-513: The layout.tsx sets page direction incorrectly by treating
Khmer (locale "km") as RTL; update the logic that computes the dir variable in
web/app/[locale]/layout.tsx (the line assigning dir based on locale) so only
"ar" yields "rtl" and all other locales (including "km") use "ltr". Locate the
const dir = ... expression and remove "km" from the RTL condition so Khmer
renders left-to-right.

In `@web/messages/pl.json`:
- Around line 106-108: The blog title key blog.posts.zenOfCmux.title is still in
English; update its value to a Polish translation so the entry is fully
localized (e.g., change "The Zen of cmux" to an appropriate Polish string) by
editing the JSON entry under "zenOfCmux" (keys: title, summary) and ensure
proper JSON string formatting and escaping.

In `@web/messages/th.json`:
- Around line 2-37: The Thai locale file contains several user-facing strings
still in English (e.g. "viewChangelog", nav key "changelog", footer key
"changelog" and other document titles like "The Zen of cmux" and "Browser
Automation"); update those keys' values to proper Thai translations so the UI is
fully localized—search for and replace occurrences of "Changelog", "The Zen of
cmux", "Browser Automation" (and any other English labels in the ranges
mentioned) with their Thai equivalents in web/messages/th.json and related
entries (preserving the JSON keys like viewChangelog, nav.changelog,
footer.changelog) ensuring placeholders like {year} remain intact.

---

Outside diff comments:
In `@web/app/`[locale]/community/page.tsx:
- Around line 71-81: The GitHub CommunityLink is hardcoded as name="GitHub";
change it to use the i18n helper (name={t("github")}) to match other links
(CommunityLink usage with t for "discord", "twitter", etc.), and ensure the
"github" key exists in the community translation namespace for all locales.

In `@web/app/`[locale]/docs/notifications/page.tsx:
- Around line 46-67: The table contains hard-coded English strings ("Variable",
"Description", "Notification title (workspace name or app name)", "Notification
subtitle", "Notification body text", and other human-readable cells) causing
partial localization; replace these literal strings with translation keys and
call the repository's translation helper (the same i18n/translator used
elsewhere in this page) for each header and cell while keeping the env var names
(CMUX_NOTIFICATION_TITLE, CMUX_NOTIFICATION_SUBTITLE, CMUX_NOTIFICATION_BODY)
and any protocol identifiers literal; apply the same change to the other table
range mentioned (lines around the second table at 107-136) so all visible copy
is pulled from translations rather than hard-coded English.

In `@web/app/`[locale]/wall-of-love/page.tsx:
- Around line 6-10: metadata currently contains hard-coded English
title/description; replace the static export const metadata with a dynamic
generator that returns localized metadata using the same wallOfLove translations
used in the page body. Implement an export async function generateMetadata({
params }) that loads the locale dictionary or translation helper (the same
source used by wallOfLove in the page) and returns a Metadata object with title
and description drawn from the wallOfLove keys; keep the Metadata type and
ensure pages for all locales (including the code currently at lines 13-23) use
the localized strings instead of literals.

---

Nitpick comments:
In `@web/app/`[locale]/blog/page.tsx:
- Around line 5-8: The metadata object is hardcoded in English; replace the
static export const metadata with an exported generateMetadata function that
returns locale-specific Metadata (e.g., export async function generateMetadata({
params }) ...) and build title/description using the page's i18n lookup (use
params.locale or your translation helper) so metadata.title and
metadata.description are localized; update any imports or helpers used by the
page component (the existing metadata symbol should be removed/replaced and
generateMetadata should return the localized title/description).

In `@web/app/`[locale]/components/blog-pager.tsx:
- Around line 6-11: Extract the duplicated blog slugs into a single shared
constant (e.g., export const blogPosts = [... ] as const) and replace local
`blogSlugs` usages with an import of that shared constant in both the
`blog-pager` component and the `blog/page` code; also derive the `slugToPath`
mapping from this shared `blogPosts` (instead of duplicating it) so `blogSlugs`
and `slugToPath` remain in sync—preserve the `as const` typing and update any
references to `blogSlugs`/`slugToPath` to use the new exported symbol.

In `@web/app/`[locale]/components/site-footer.tsx:
- Around line 84-89: Move the dynamic year computation out of the component
render and memoize it as a module-level constant (e.g., const year = new
Date().getFullYear()) so new Date().getFullYear() is not called on every render;
update the usage in site-footer.tsx where {t("copyright", { year })} is
referenced and leave LanguageSwitcher and the component unchanged.

In `@web/app/`[locale]/docs/api/page.tsx:
- Around line 6-10: The exported static constant metadata is not localized;
replace the static export (export const metadata) with an async generateMetadata
function that uses your i18n/translation helper to produce title and description
per locale (matching how the page content is localized), e.g., implement
generateMetadata({ params }) to read params.locale and return a Metadata object
with translated title/description so SEO/meta tags match localized content;
update any imports/types if needed to support async generateMetadata and remove
the static metadata export.

In `@web/app/`[locale]/docs/concepts/page.tsx:
- Around line 5-9: Replace the static metadata export with a locale-aware
generateMetadata function: remove or replace the exported const metadata and
implement an exported async function generateMetadata({ params }) that awaits
params.locale, calls getTranslations with the locale and the "docs.concepts"
namespace (or appropriate namespace), and returns title and description from the
translation keys (e.g., "title" and "metaDescription"); ensure you import
getTranslations from "next-intl/server" and keep the returned type as
Promise<Metadata> so metadata is generated per-locale.

In `@web/app/`[locale]/docs/keyboard-shortcuts/page.tsx:
- Around line 5-9: The exported constant metadata is hardcoded in English;
replace it with a localized metadata provider by implementing generateMetadata
to return title/description using the existing t(...) translation function
instead of the static export; update/remove the export const metadata and add an
async function generateMetadata({ params, locale }) that calls your i18n
t("title") and t("description") (or the same translation hook used in the page)
and returns the localized Metadata object so metadata matches the page
translations.

In `@web/app/sitemap.ts`:
- Around line 7-22: The paths array currently sets lastModified using new
Date(), which makes builds non-deterministic; update the lastModified entries in
the paths array (and any other occurrences) to use a static build-time value
(e.g., a BUILD_DATE constant sourced from process.env.BUILD_DATE or a fixed ISO
date string) so only pages that actually change get a new date; locate the paths
constant in sitemap.ts and replace new Date() uses with the BUILD_DATE constant
(or explicit static date strings) and ensure BUILD_DATE is defined and used
consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dbd7026b-411c-4e78-a6b6-910ef4418f05

📥 Commits

Reviewing files that changed from the base of the PR and between f71c2c1 and 79eacda.

⛔ Files ignored due to path filters (3)
  • web/app/[locale]/assets/landing-image.png is excluded by !**/*.png
  • web/app/[locale]/blog/show-hn-launch/star-history.png is excluded by !**/*.png
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (87)
  • web/app/[locale]/(legal)/eula/page.tsx
  • web/app/[locale]/(legal)/layout.tsx
  • web/app/[locale]/(legal)/privacy-policy/page.tsx
  • web/app/[locale]/(legal)/terms-of-service/page.tsx
  • web/app/[locale]/assets/images.d.ts
  • web/app/[locale]/blog/cmd-shift-u/page.tsx
  • web/app/[locale]/blog/introducing-cmux/page.tsx
  • web/app/[locale]/blog/layout.tsx
  • web/app/[locale]/blog/page.tsx
  • web/app/[locale]/blog/show-hn-launch/page.tsx
  • web/app/[locale]/blog/zen-of-cmux/page.tsx
  • web/app/[locale]/community/page.tsx
  • web/app/[locale]/components/blog-cta.tsx
  • web/app/[locale]/components/blog-pager.tsx
  • web/app/[locale]/components/blog-posts.ts
  • web/app/[locale]/components/callout.tsx
  • web/app/[locale]/components/code-block.tsx
  • web/app/[locale]/components/docs-nav-items.ts
  • web/app/[locale]/components/docs-pager.tsx
  • web/app/[locale]/components/docs-sidebar.tsx
  • web/app/[locale]/components/download-button.tsx
  • web/app/[locale]/components/fade-image.tsx
  • web/app/[locale]/components/github-button.tsx
  • web/app/[locale]/components/github-stars.tsx
  • web/app/[locale]/components/language-switcher.tsx
  • web/app/[locale]/components/mobile-drawer.tsx
  • web/app/[locale]/components/nav-links.tsx
  • web/app/[locale]/components/site-footer.tsx
  • web/app/[locale]/components/site-header.tsx
  • web/app/[locale]/components/spacing-control.tsx
  • web/app/[locale]/docs/api/page.tsx
  • web/app/[locale]/docs/browser-automation/page.tsx
  • web/app/[locale]/docs/changelog/changelog-media.ts
  • web/app/[locale]/docs/changelog/page.tsx
  • web/app/[locale]/docs/concepts/page.tsx
  • web/app/[locale]/docs/configuration/page.tsx
  • web/app/[locale]/docs/docs-nav.tsx
  • web/app/[locale]/docs/getting-started/page.tsx
  • web/app/[locale]/docs/keyboard-shortcuts/page.tsx
  • web/app/[locale]/docs/layout.tsx
  • web/app/[locale]/docs/notifications/page.tsx
  • web/app/[locale]/docs/page.tsx
  • web/app/[locale]/keyboard-shortcuts.tsx
  • web/app/[locale]/layout.tsx
  • web/app/[locale]/page.tsx
  • web/app/[locale]/posthog.tsx
  • web/app/[locale]/providers.tsx
  • web/app/[locale]/testimonials.tsx
  • web/app/[locale]/theme.tsx
  • web/app/[locale]/typing.tsx
  • web/app/[locale]/wall-of-love/page.tsx
  • web/app/blog/introducing-cmux/page.tsx
  • web/app/blog/page.tsx
  • web/app/blog/show-hn-launch/page.tsx
  • web/app/blog/zen-of-cmux/page.tsx
  • web/app/components/docs-nav-items.ts
  • web/app/docs/concepts/page.tsx
  • web/app/docs/getting-started/page.tsx
  • web/app/keyboard-shortcuts.tsx
  • web/app/layout.tsx
  • web/app/page.tsx
  • web/app/sitemap.ts
  • web/i18n/navigation.ts
  • web/i18n/request.ts
  • web/i18n/routing.ts
  • web/messages/ar.json
  • web/messages/bs.json
  • web/messages/da.json
  • web/messages/de.json
  • web/messages/en.json
  • web/messages/es.json
  • web/messages/fr.json
  • web/messages/it.json
  • web/messages/ja.json
  • web/messages/km.json
  • web/messages/ko.json
  • web/messages/no.json
  • web/messages/pl.json
  • web/messages/pt-BR.json
  • web/messages/ru.json
  • web/messages/th.json
  • web/messages/tr.json
  • web/messages/zh-CN.json
  • web/messages/zh-TW.json
  • web/middleware.ts
  • web/next.config.ts
  • web/package.json
💤 Files with no reviewable changes (9)
  • web/app/components/docs-nav-items.ts
  • web/app/blog/show-hn-launch/page.tsx
  • web/app/keyboard-shortcuts.tsx
  • web/app/blog/introducing-cmux/page.tsx
  • web/app/docs/concepts/page.tsx
  • web/app/blog/zen-of-cmux/page.tsx
  • web/app/page.tsx
  • web/app/blog/page.tsx
  • web/app/docs/getting-started/page.tsx

Comment thread web/app/[locale]/(legal)/eula/page.tsx Outdated
Comment on lines 11 to 15
const t = useTranslations("legal");
return (
<>
<h1>Privacy Policy</h1>
<h1>{t("privacyPolicy")}</h1>
<p>Last updated: December 2, 2025</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This policy is still only partially localized.

The heading moved to next-intl, but the body copy — including the inline “Terms of Service” link text you just touched — remains hard-coded English. That makes /[locale]/privacy-policy look translated when it is not.

Also applies to: 35-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/(legal)/privacy-policy/page.tsx around lines 11 - 15, The
page currently uses useTranslations("legal") for the heading but leaves the body
copy and the inline "Terms of Service" link text hard-coded English; update the
component in page.tsx to replace the hard-coded paragraph and the link text with
translation lookups using the t function (e.g., t("privacyPolicyLastUpdated"),
t("privacyPolicyBody"), and t("termsOfServiceLinkText") or similar keys) and
ensure those keys are added to the locale resource files for all supported
locales so the full body and link are localized; keep the existing structure and
link href but use t(...) for all user-visible strings in the component
(references: useTranslations, t in page.tsx).

Comment on lines +10 to +13
const t = useTranslations("legal");
return (
<>
<h1>Terms of Service</h1>
<h1>{t("termsOfService")}</h1>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t expose a localized Terms route with English terms.

Only the <h1> is translated here; the actual Terms of Service remain hard-coded English. That makes the locale route look translated when it is not, which is especially risky for legal content.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/(legal)/terms-of-service/page.tsx around lines 10 - 13, The
page uses useTranslations("legal") only for the <h1> (t("termsOfService")) while
the Terms of Service body remains hard-coded English; update the component to
source the full TOS content from your localization system instead of inline
English — either replace the hard-coded text with translation keys accessed via
useTranslations (e.g., t("termsBody", { /* or t("termsParagraph1")... */ })) or
load a locale-specific markdown/JSON file and render it; ensure all text
currently in the body is mapped to translation keys or locale assets and
rendered via the same translated-source (symbols to change: useTranslations, t,
and the page component rendering the body).

Comment thread web/app/[locale]/blog/introducing-cmux/page.tsx Outdated
Comment thread web/app/[locale]/blog/introducing-cmux/page.tsx
Comment thread web/messages/da.json
"p2": "Cmd+Shift+U springer til den nyeste ulæste <link>notifikation</link>. I praksis betyder det den sidste agent der blev færdig. Den skifter til det rigtige workspace, fokuserer det præcise panel, flasher det så du kan se hvor du skal kigge, og markerer det som læst. Hvis notifikationen kom fra et andet vindue, kommer det vindue forrest."
},
"zenOfCmux": {
"title": "The Zen of cmux",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Finish the remaining English headings.

Line 107 (The Zen of cmux) and Line 507 (Wall of Love) are still English while the surrounding locale is Danish, so those titles will ship untranslated in the Danish UI.

Also applies to: 507-507

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/da.json` at line 107, Two heading strings in the Danish locale
are still English: replace the values "The Zen of cmux" and "Wall of Love" in
the da.json entries so they are translated into Danish; locate the JSON objects
where "title": "The Zen of cmux" and "title": "Wall of Love" appear and update
their "title" values to the correct Danish translations (e.g., "Zen af cmux" or
a preferred Danish phrasing, and "Kærlighedens mur" or preferred localized
phrasing) ensuring the quotes and JSON formatting remain valid.

Comment thread web/messages/da.json Outdated
Comment on lines +203 to +233
"paneTitle": "Panel",
"paneDesc": "Et opdelt område inden for et workspace. Oprettes ved at opdele med {right} (højre) eller {down} (ned). Navigér mellem paneler med {nav} + piletaster.",
"paneNote": "Hvert panel kan indeholde flere surfaces (faner inden for panelet).",
"surfaceTitle": "Surface",
"surfaceDesc": "En fane inden for et panel. Hvert panel har sin egen fanebjælke og kan indeholde flere surfaces. Oprettes med {new}, navigeres med {prev} / {next} eller {jump}.",
"surfaceNote": "Surfaces er de individuelle terminal- eller browsersessioner du interagerer med. Hver surface har sin egen CMUX_SURFACE_ID-miljøvariabel.",
"panelTitle": "Panel",
"panelDesc": "Indholdet inde i en surface. Aktuelt to typer:",
"panelTerminal": "Terminal: en Ghostty-terminalsession",
"panelBrowser": "Browser: en indlejret webvisning",
"panelNote": "Panel er primært et internt koncept. I socket API og CLI interagerer du med surfaces snarere end paneler direkte.",
"visualExample": "Visuelt eksempel",
"visualExampleDesc": "I dette eksempel:",
"visualItem1": "Vinduet indeholder en sidebar med tre workspaces (dev, server, logs)",
"visualItem2": "Workspace \"dev\" er valgt og viser to paneler side om side",
"visualItem3": "Panel 1 har to surfaces ([S1] og [S2] i fanebjælken), med S1 aktiv",
"visualItem4": "Panel 2 har én surface",
"visualItem5": "Hver surface indeholder et panel (en terminal i dette tilfælde)",
"summary": "Oversigt",
"levelHeader": "Niveau",
"whatItIsHeader": "Hvad det er",
"createdByHeader": "Oprettet af",
"identifiedByHeader": "Identificeret ved",
"macosWindow": "macOS-vindue",
"sidebarEntry": "Sidebarpost",
"splitRegion": "Opdelt område",
"tabWithinPane": "Fane inden for panel",
"terminalOrBrowser": "Terminal eller browser",
"automatic": "Automatisk",
"paneIdSocket": "Panel-ID (socket API)",
"panelIdInternal": "Panel-ID (internt)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep pane and panel distinct in the concepts section.

Line 203 and Line 209 both render as Panel, and Line 232 and Line 233 both become Panel-ID. The concepts page uses these as different hierarchy levels, so the Danish copy currently collapses an important distinction and makes the docs/API terminology ambiguous.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/da.json` around lines 203 - 233, The Danish file conflates
"pane" and "panel" (making both render as "Panel"); update the affected keys so
the two concepts remain distinct — specifically change paneTitle, paneDesc,
paneNote, paneIdSocket to use a distinct Danish term for "pane" (e.g.,
"Opdelingspanel" or "Pane" with clarifying wording) while keeping panelTitle,
panelDesc, panelNote, panelTerminal, panelBrowser, panelIdInternal as the
separate "panel" term; ensure descriptions (paneDesc vs panelDesc) and the ID
labels (paneIdSocket vs panelIdInternal) clearly reflect their different
hierarchy levels without altering other keys.

Comment thread web/messages/km.json
Comment thread web/messages/pl.json
Comment on lines +106 to +108
"zenOfCmux": {
"title": "The Zen of cmux",
"summary": "cmux to prymityw, nie rozwiązanie. Daje ci komponowalne elementy, a Twój workflow zależy od Ciebie.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize this blog title as well.

blog.posts.zenOfCmux.title is still English while the rest of the entry is Polish, so this locale will render mixed-language copy in the blog UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/pl.json` around lines 106 - 108, The blog title key
blog.posts.zenOfCmux.title is still in English; update its value to a Polish
translation so the entry is fully localized (e.g., change "The Zen of cmux" to
an appropriate Polish string) by editing the JSON entry under "zenOfCmux" (keys:
title, summary) and ensure proper JSON string formatting and escaping.

Comment thread web/messages/th.json

@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: a8748f16b2

ℹ️ 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 web/app/[locale]/page.tsx Outdated
Comment on lines +110 to +114
<a href="/docs/keyboard-shortcuts" className={linkClass}>
{t.rich("feature.keyboardShortcutsDesc", {
link: (chunks) => (
<a
href="/docs/keyboard-shortcuts"

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 Remove nested anchor in keyboard shortcuts feature text

t.rich("feature.keyboardShortcutsDesc", …) already renders a <link>…</link> placeholder as an anchor, but this block wraps that output in another <a>; this creates invalid nested anchors on the homepage for every locale, which can trigger React DOM nesting warnings and unreliable click behavior in browsers. It also duplicates punctuation because the message itself already includes the leading :.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/app/`[locale]/page.tsx:
- Around line 13-19: The Home component declares params but never awaits or uses
it; to enable SSG locale support call setRequestLocale with the resolved locale
before rendering. Update the Home function to await params (or accept it as an
awaited value), extract locale, call setRequestLocale(locale) (from next-intl)
and then return <HomeContent />; reference the Home component, its params
parameter, and setRequestLocale so the locale is set at build time for static
generation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d5238d3f-dbb5-4751-845f-0cba5e690cc0

📥 Commits

Reviewing files that changed from the base of the PR and between 79eacda and f22779a.

⛔ Files ignored due to path filters (1)
  • web/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • web/app/[locale]/page.tsx

Comment thread web/app/[locale]/page.tsx Outdated
Comment on lines +13 to +19
export default function Home({
params,
}: {
params: Promise<{ locale: string }>;
}) {
return <HomeContent />;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Call setRequestLocale for SSG support.

The params prop is declared but never used. With next-intl v4, you must await the params and call setRequestLocale(locale) to enable static generation of locale-specific pages. Without this, next-intl cannot determine the locale during build time.

🛠️ Proposed fix
-export default function Home({
+export default async function Home({
   params,
 }: {
   params: Promise<{ locale: string }>;
 }) {
+  const { locale } = await params;
+  setRequestLocale(locale);
   return <HomeContent />;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/page.tsx around lines 13 - 19, The Home component declares
params but never awaits or uses it; to enable SSG locale support call
setRequestLocale with the resolved locale before rendering. Update the Home
function to await params (or accept it as an awaited value), extract locale,
call setRequestLocale(locale) (from next-intl) and then return <HomeContent />;
reference the Home component, its params parameter, and setRequestLocale so the
locale is set at build time for static generation.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

♻️ Duplicate comments (4)
web/messages/th.json (1)

10-35: ⚠️ Potential issue | 🟡 Minor

Finish localizing the remaining Thai strings.

This catalog still exposes several non-brand English labels in the Thai locale, including Changelog, The Zen of cmux, Browser Automation, and multiple subsection headings under docs.browserAutomation. Those will surface as mixed-language UI in nav, footer, blog, and docs.

Also applies to: 106-107, 447-499

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/th.json` around lines 10 - 35, Several UI strings in the Thai
locale remain untranslated (e.g., keys like "viewChangelog", "nav.changelog",
footer keys "changelog", "blog", "github"/"twitter"/"discord" labels, and doc
headings under docs.browserAutomation such as "The Zen of cmux" and "Browser
Automation"), causing mixed-language UI; update the Thai JSON entries for those
keys and any referenced subsection headings (including the repeated ranges
noted) with proper Thai translations, ensuring consistency for nav.changelog,
footer.changelog, viewChangelog, blog, community, docs.browserAutomation.* and
the specific titles "The Zen of cmux" and "Browser Automation" so the UI is
fully localized.
web/app/[locale]/page.tsx (2)

13-18: ⚠️ Potential issue | 🟠 Major

Use setRequestLocale(locale) before rendering.

params is never awaited and the imported setRequestLocale stays unused, so this page still isn't setting the request locale for locale-specific SSG.

Suggested fix
-export default function Home({
+export default async function Home({
   params,
 }: {
   params: Promise<{ locale: string }>;
 }) {
+  const { locale } = await params;
+  setRequestLocale(locale);
   return <HomeContent />;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/page.tsx around lines 13 - 18, The Home page never awaits
the params Promise nor calls the imported setRequestLocale, so extract the
locale by awaiting params in the Home function (params:
Promise<{locale:string}>), then call setRequestLocale(locale) before rendering;
i.e., await params to get {locale}, invoke setRequestLocale(locale), and only
then return <HomeContent /> so SSG uses the correct request locale.

108-121: ⚠️ Potential issue | 🟠 Major

Use the locale-aware Link for internal docs links and remove the extra separator.

These /docs/... anchors still bypass the next-intl navigation wrapper, and Lines 110-121 still render an <a> inside another <a>. Also, feature.keyboardShortcutsDesc already includes the leading :, so the hardcoded separator here duplicates punctuation across locales.

Suggested fix
-                <span className="text-muted">
-                  :{" "}
-                  <a href="/docs/keyboard-shortcuts" className={linkClass}>
-                    {t.rich("feature.keyboardShortcutsDesc", {
-                      link: (chunks) => (
-                        <a
-                          href="/docs/keyboard-shortcuts"
-                          className={linkClass}
-                        >
-                          {chunks}
-                        </a>
-                      ),
-                    })}
-                  </a>
-                </span>
+                <span className="text-muted">
+                  {t.rich("feature.keyboardShortcutsDesc", {
+                    link: (chunks) => (
+                      <Link
+                        href="/docs/keyboard-shortcuts"
+                        className={linkClass}
+                      >
+                        {chunks}
+                      </Link>
+                    ),
+                  })}
+                </span>
-                  cliLink: (chunks) => (
-                    <a href="/docs/notifications" className={linkClass}>
-                      {chunks}
-                    </a>
-                  ),
-                  hooksLink: (chunks) => (
-                    <a href="/docs/notifications" className={linkClass}>
-                      {chunks}
-                    </a>
-                  ),
+                  cliLink: (chunks) => (
+                    <Link href="/docs/notifications" className={linkClass}>
+                      {chunks}
+                    </Link>
+                  ),
+                  hooksLink: (chunks) => (
+                    <Link href="/docs/notifications" className={linkClass}>
+                      {chunks}
+                    </Link>
+                  ),
-                  link: (chunks) => (
-                    <a href="/docs/keyboard-shortcuts" className={linkClass}>
-                      {chunks}
-                    </a>
-                  ),
+                  link: (chunks) => (
+                    <Link
+                      href="/docs/keyboard-shortcuts"
+                      className={linkClass}
+                    >
+                      {chunks}
+                    </Link>
+                  ),

Also applies to: 177-205

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/page.tsx around lines 108 - 121, Replace the plain anchors
with the locale-aware Link and remove the duplicated separator and nested
anchors: remove the hardcoded ": " before t.rich (since
feature.keyboardShortcutsDesc already contains the punctuation), change the
outer <a href="/docs/keyboard-shortcuts" className={linkClass}> to a
locale-aware <Link href="/docs/keyboard-shortcuts" className={linkClass}>, and
update the t.rich link renderer so it returns a single <Link
href="/docs/keyboard-shortcuts" className={linkClass}>{chunks}</Link> instead of
an inner <a>; apply the same edits to the other occurrence noted (the block
around lines 177-205). Use the existing linkClass and t.rich identifiers to
locate the elements in page.tsx.
web/messages/pl.json (1)

106-108: ⚠️ Potential issue | 🟡 Minor

Translate zenOfCmux.title too.

This entry is still English, so the Polish blog card/page will render mixed-language copy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/pl.json` around lines 106 - 108, The title for the localization
key zenOfCmux.title is still in English; update the value of zenOfCmux.title in
the JSON to a proper Polish translation (matching the existing Polish summary),
e.g., replace "The Zen of cmux" with an appropriate Polish string so the blog
card/page no longer mixes languages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/messages/de.json`:
- Around line 106-108: The title key zenOfCmux.title is still in English; update
its value to a German translation so the UI is fully localized (e.g., replace
"The Zen of cmux" with an appropriate German string such as "Die Zen von cmux"
or better "Das Zen von cmux" or another natural German phrasing) by editing the
zenOfCmux.title entry in the de.json translations.

In `@web/messages/en.json`:
- Around line 109-110: Update the English source string zenOfCmux.p1 to fix the
typo: change "how developers hold their tools" to "how developers use their
tools" (i.e., edit the value for the "p1" key under "zenOfCmux" so it reads
"...about how developers use their tools.").

In `@web/messages/es.json`:
- Around line 106-108: The translation for zenOfCmux.title is still in English;
update the value for the key "zenOfCmux.title" in the Spanish messages to a
Spanish translation (for example "El zen de cmux" or another appropriate
localized title) so the blog entry is fully localized and consistent with
"zenOfCmux.summary".

In `@web/messages/ja.json`:
- Around line 106-108: The JSON key "zenOfCmux" has an English "title" while its
"summary" and body are Japanese; update the "title" value for zenOfCmux to a
proper Japanese translation to avoid mixed-language rendering (locate the
"zenOfCmux" object and replace the "title" string with the Japanese equivalent).

---

Duplicate comments:
In `@web/app/`[locale]/page.tsx:
- Around line 13-18: The Home page never awaits the params Promise nor calls the
imported setRequestLocale, so extract the locale by awaiting params in the Home
function (params: Promise<{locale:string}>), then call setRequestLocale(locale)
before rendering; i.e., await params to get {locale}, invoke
setRequestLocale(locale), and only then return <HomeContent /> so SSG uses the
correct request locale.
- Around line 108-121: Replace the plain anchors with the locale-aware Link and
remove the duplicated separator and nested anchors: remove the hardcoded ": "
before t.rich (since feature.keyboardShortcutsDesc already contains the
punctuation), change the outer <a href="/docs/keyboard-shortcuts"
className={linkClass}> to a locale-aware <Link href="/docs/keyboard-shortcuts"
className={linkClass}>, and update the t.rich link renderer so it returns a
single <Link href="/docs/keyboard-shortcuts"
className={linkClass}>{chunks}</Link> instead of an inner <a>; apply the same
edits to the other occurrence noted (the block around lines 177-205). Use the
existing linkClass and t.rich identifiers to locate the elements in page.tsx.

In `@web/messages/pl.json`:
- Around line 106-108: The title for the localization key zenOfCmux.title is
still in English; update the value of zenOfCmux.title in the JSON to a proper
Polish translation (matching the existing Polish summary), e.g., replace "The
Zen of cmux" with an appropriate Polish string so the blog card/page no longer
mixes languages.

In `@web/messages/th.json`:
- Around line 10-35: Several UI strings in the Thai locale remain untranslated
(e.g., keys like "viewChangelog", "nav.changelog", footer keys "changelog",
"blog", "github"/"twitter"/"discord" labels, and doc headings under
docs.browserAutomation such as "The Zen of cmux" and "Browser Automation"),
causing mixed-language UI; update the Thai JSON entries for those keys and any
referenced subsection headings (including the repeated ranges noted) with proper
Thai translations, ensuring consistency for nav.changelog, footer.changelog,
viewChangelog, blog, community, docs.browserAutomation.* and the specific titles
"The Zen of cmux" and "Browser Automation" so the UI is fully localized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a31e3438-a7a8-49b8-a93f-638d2a16263d

📥 Commits

Reviewing files that changed from the base of the PR and between f22779a and 295c4f9.

📒 Files selected for processing (20)
  • web/app/[locale]/page.tsx
  • web/messages/ar.json
  • web/messages/bs.json
  • web/messages/da.json
  • web/messages/de.json
  • web/messages/en.json
  • web/messages/es.json
  • web/messages/fr.json
  • web/messages/it.json
  • web/messages/ja.json
  • web/messages/km.json
  • web/messages/ko.json
  • web/messages/no.json
  • web/messages/pl.json
  • web/messages/pt-BR.json
  • web/messages/ru.json
  • web/messages/th.json
  • web/messages/tr.json
  • web/messages/zh-CN.json
  • web/messages/zh-TW.json
✅ Files skipped from review due to trivial changes (2)
  • web/messages/it.json
  • web/messages/ru.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • web/messages/zh-TW.json
  • web/messages/fr.json
  • web/messages/zh-CN.json
  • web/messages/ar.json
  • web/messages/bs.json
  • web/messages/tr.json

Comment thread web/messages/de.json
Comment thread web/messages/en.json
Comment on lines +109 to +110
"date": "February 27, 2026",
"p1": "cmux is not prescriptive about how developers hold their tools. It's a terminal and browser with a CLI, and the rest is up to you.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the typo in zenOfCmux.p1.

“how developers hold their tools” reads wrong in the English source text; this likely meant “use their tools.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/en.json` around lines 109 - 110, Update the English source
string zenOfCmux.p1 to fix the typo: change "how developers hold their tools" to
"how developers use their tools" (i.e., edit the value for the "p1" key under
"zenOfCmux" so it reads "...about how developers use their tools.").

Comment thread web/messages/es.json
Comment thread web/messages/ja.json

@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

https://github.com/manaflow-ai/cmux/blob/295c4f9e37198c53897045dd6b8753e735bd105a/web/app/[locale]/docs/page.tsx#L4
P2 Badge Preserve locale when redirecting docs index

Inside the localized docs route, redirecting to absolute /docs/getting-started drops the current locale from /<locale>/docs requests. This forces an extra middleware round-trip for non-default languages and can fall back to English when locale cookies are unavailable, so users entering a localized docs index may be sent to the default-language docs instead of staying in their selected locale.

ℹ️ 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 web/middleware.ts
export default createMiddleware(routing);

export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],

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 Exclude PostHog proxy routes from locale middleware

The matcher currently internationalizes every non-file path, which includes /cmuxterm/*; however, web/next.config.ts depends on /cmuxterm/:path* rewrites to forward analytics traffic to PostHog. For non-default locales, middleware will redirect endpoints like /cmuxterm/e/ to /<locale>/cmuxterm/e/, and that prefixed path no longer matches the rewrite, so event ingestion requests can 404 and analytics/experiment data is lost for localized sessions.

Useful? React with 👍 / 👎.

…eholders

Changed {legacy}, {openShortcut}, {jumpShortcut} from plain variable
interpolation to <tag>content</tag> format so t.rich() gets proper
functions instead of values.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
web/app/[locale]/docs/notifications/page.tsx (2)

46-75: ⚠️ Potential issue | 🟡 Minor

Several labels in this page are still hard-coded English.

The custom-command table, the “Examples” title, and the OSC comparison rows remain English, so non-English locales will still see mixed-language docs in the middle of an otherwise translated page.

Also applies to: 107-137

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/docs/notifications/page.tsx around lines 46 - 75, The table
headers and cell labels (the <th>Variable</th>, <th>Description</th> and the
table rows showing CMUX_NOTIFICATION_TITLE/SUBTITLE/BODY) plus the CodeBlock
title="Examples" and the OSC comparison rows (later in this file) are hard‑coded
in English; replace these literal strings with calls to the existing
i18n/translation helper used in this file (e.g., the getDictionary/t function or
<Trans> wrapper used elsewhere in page.tsx) so the table headers, cell
descriptions, the CodeBlock title, and the OSC comparison labels are rendered
from the locale dictionary; update the strings keys and add corresponding
entries to the locale JSONs as needed and ensure CodeBlock title prop receives
the translated value (refer to the CodeBlock component and the table markup in
page.tsx to locate each place to change).

6-10: ⚠️ Potential issue | 🟠 Major

Localize the metadata export for this route.

The page uses useTranslations("docs.notifications") for visible content, but the static metadata at lines 6-10 remains hardcoded in English. Non-English routes will therefore display English <title> and description in the browser despite locale catalogs already containing localized values for docs.notifications.title and docs.notifications.metaDescription.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/docs/notifications/page.tsx around lines 6 - 10, Replace
the hardcoded export const metadata: Metadata with a locale-aware metadata
generator: implement an async generateMetadata({ params }) that reads the route
locale (params.locale) and resolves the localized strings for
docs.notifications.title and docs.notifications.metaDescription (the same keys
used by useTranslations("docs.notifications")), then return a Metadata object
with those values; update any imports to use the server-side i18n/translator
utility you use for other routes so the browser title and description are
localized per-locale.
web/app/[locale]/docs/api/page.tsx (1)

6-10: ⚠️ Potential issue | 🟠 Major

Localize metadata in all docs pages using generateMetadata.

The page body uses useTranslations("docs.api"), but the exported metadata object at lines 6-10 is hard-coded English. Non-English routes will render with English <title> and description in the HTML head, even though locale catalogs provide localized docs.api.title and docs.api.metaDescription (and equivalent strings for other docs sections).

Replace the static metadata export with a generateMetadata function that retrieves localized strings from the appropriate locale catalog. The same issue affects all other docs pages (api, browser-automation, changelog, concepts, configuration, notifications, keyboard-shortcuts, getting-started).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/docs/api/page.tsx around lines 6 - 10, The exported static
metadata object (export const metadata) must be replaced with an async
generateMetadata function that loads the locale-specific translations and
returns localized title and description; specifically, remove export const
metadata and implement export async function generateMetadata({ params }) {
const t = await getTranslatorForLocale(params.locale) } (or use the same i18n
helper used by useTranslations("docs.api")) and return { title:
t("docs.api.title"), description: t("docs.api.metaDescription") } so the HTML
head uses docs.api.title and docs.api.metaDescription for the current locale;
apply the same pattern to the other docs pages (browser-automation, changelog,
concepts, configuration, notifications, keyboard-shortcuts, getting-started).
♻️ Duplicate comments (5)
web/messages/es.json (1)

107-107: ⚠️ Potential issue | 🟡 Minor

Translate zenOfCmux.title in the Spanish catalog.

The summary and body are Spanish, but the title stays English, so this post renders mixed-language copy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/es.json` at line 107, The Spanish messages file contains an
untranslated title for "zenOfCmux.title" — update the value for the "title" key
under zenOfCmux in web/messages/es.json to its Spanish equivalent (e.g.,
translate "The Zen of cmux" into Spanish) so the title matches the existing
Spanish summary/body; ensure the JSON string value is properly escaped and
preserves the key "zenOfCmux.title".
web/messages/ja.json (1)

107-107: ⚠️ Potential issue | 🟡 Minor

Localize the zenOfCmux title as well.

The summary and body are Japanese, but the title is still English, so this post renders mixed-language copy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/ja.json` at line 107, The "zenOfCmux" entry has its "title"
still in English while "summary" and "body" are Japanese; update the zenOfCmux
-> title value in the ja.json localization to a proper Japanese translation to
avoid mixed-language rendering and keep the key name "zenOfCmux" and "title"
unchanged.
web/messages/en.json (1)

109-110: ⚠️ Potential issue | 🟡 Minor

Fix the typo in zenOfCmux.p1.

“hold their tools” reads wrong in the source copy and should be “use their tools”.

✏️ Proposed fix
-        "p1": "cmux is not prescriptive about how developers hold their tools. It's a terminal and browser with a CLI, and the rest is up to you.",
+        "p1": "cmux is not prescriptive about how developers use their tools. It's a terminal and browser with a CLI, and the rest is up to you.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/en.json` around lines 109 - 110, Update the translation string
for zenOfCmux.p1 by replacing the incorrect phrase "hold their tools" with "use
their tools" so the value of the "p1" key reads: "cmux is not prescriptive about
how developers use their tools. It's a terminal and browser with a CLI, and the
rest is up to you." Locate the "p1" key under zenOfCmux (or the matching JSON
entry containing that sentence) and make the word substitution.
web/messages/da.json (2)

203-233: ⚠️ Potential issue | 🟠 Major

Keep pane and panel distinct in the Danish concepts glossary.

Both hierarchy levels render as Panel, and both IDs become Panel-ID. That collapses a real docs/API distinction and makes the concepts page ambiguous.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/da.json` around lines 203 - 233, The Danish translation
conflates the terms "pane" and "panel" making both display as "Panel" and both
IDs as "Panel-ID"; update the entries to preserve the distinction by choosing
distinct Danish terms (e.g., keep "pane" => "Panel" and translate "panel" =>
"Underpanel" or similar) and adjust ID labels accordingly so "paneIdSocket" and
"panelIdInternal" reflect different names; modify the values for paneTitle,
panelTitle, paneDesc, panelDesc, paneNote, panelNote, paneIdSocket, and
panelIdInternal to use the distinct Danish words consistently across
descriptions.

107-107: ⚠️ Potential issue | 🟡 Minor

Translate the remaining English headings.

blog.posts.zenOfCmux.title and wallOfLove.title are still English, so the Danish locale will ship mixed-language headings in those views.

Also applies to: 507-507

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/da.json` at line 107, The Danish locale still contains English
headings: update the locale keys "blog.posts.zenOfCmux.title" and
"wallOfLove.title" to Danish strings so the views are fully localized; locate
those keys in the translations file and replace the English values ("The Zen of
cmux" and the current "wallOfLove" title) with appropriate Danish translations
(e.g., translate to "Zen af cmux" or another correct Danish phrasing) ensuring
the JSON remains valid.
🧹 Nitpick comments (1)
web/messages/it.json (1)

203-205: Use distinct Italian terms for Pane and Panel.

Line 203 translates Pane as Pannello, but Lines 209-213 then keep Panel/panel in English. On this concepts page those are different objects, so the current wording makes the hierarchy harder to follow and weakens the API/docs mapping.

💡 Example wording to keep the two concepts separate
-      "paneTitle": "Pannello",
+      "paneTitle": "Riquadro",
...
-      "panelTitle": "Panel",
+      "panelTitle": "Pannello",
...
-      "paneIdSocket": "ID pannello (API socket)",
+      "paneIdSocket": "ID riquadro (API socket)",
-      "panelIdInternal": "ID panel (interno)"
+      "panelIdInternal": "ID pannello (interno)"

Also applies to: 209-213, 220-233

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/it.json` around lines 203 - 205, Summary: Use distinct Italian
terms for the two concepts so "Pane" and "Panel" don't collapse into the same
word. Fix: in the JSON entries related to the Pane concept (keys paneTitle,
paneDesc, paneNote) replace the current translation "Pannello" with a distinct
Italian term (e.g., use "Riquadro" or "Area del pannello" for Pane) and ensure
all other keys that represent the separate Panel concept keep "Pannello" (or
another consistently chosen term for Panel); update all occurrences of pane* and
any other strings referring to Pane across the file (also where panel strings
appear) so the two concepts use different, consistent Italian terms and preserve
the original placeholders like {right}, {down}, {nav}.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/messages/es.json`:
- Line 507: The JSON value for wallOfLove.title is still in English; update the
"wallOfLove.title" entry in web/messages/es.json to its Spanish translation
(e.g., "Muro de amor" or your approved localized string) so the Spanish locale
no longer mixes languages and matches other localized keys like wallOfLove.*.
- Around line 203-233: The Spanish translations collapse two distinct
concepts—pane and panel—into the same words; update the JSON so keys referencing
the container region (paneTitle, paneDesc, paneNote, paneIdSocket) use a
distinct term (e.g., "Región" or "Panel dividido") and keys referencing the
content unit (panelTitle, panelDesc, panelTerminal, panelBrowser, panelNote,
panelIdInternal) use a different term (e.g., "Panel" or "Contenido de panel");
ensure pane* and panel* keys use consistent, different Spanish strings
throughout so the hierarchy remains unambiguous.

In `@web/messages/pt-BR.json`:
- Around line 209-233: The Portuguese text mixes English fragments for the
borrowed term "Panel" and inconsistent uses of "panel"/"painel"; update the
strings to be fully natural Portuguese while keeping "Panel" as the distinct
borrowed term (and explain it once), by editing keys like "panelTitle",
"panelDesc", "panelTerminal", "panelBrowser", "panelNote", "panelNote",
"visualItem5", and "panelIdInternal" to (1) introduce "Panel" as a borrowed term
in Portuguese (e.g. "Panel (termo emprestado, distinto de 'painel')"), (2)
translate remaining English fragments and plurals into Portuguese (e.g. replace
"panels" with "Panels" or "Panel(s)" consistently and render surrounding
verbs/nouns in Portuguese), and (3) make "panelIdInternal" a Portuguese label
that clarifies it's an internal ID for a Panel; ensure capitalization and
pluralization of "Panel" are consistent across these keys.

---

Outside diff comments:
In `@web/app/`[locale]/docs/api/page.tsx:
- Around line 6-10: The exported static metadata object (export const metadata)
must be replaced with an async generateMetadata function that loads the
locale-specific translations and returns localized title and description;
specifically, remove export const metadata and implement export async function
generateMetadata({ params }) { const t = await
getTranslatorForLocale(params.locale) } (or use the same i18n helper used by
useTranslations("docs.api")) and return { title: t("docs.api.title"),
description: t("docs.api.metaDescription") } so the HTML head uses
docs.api.title and docs.api.metaDescription for the current locale; apply the
same pattern to the other docs pages (browser-automation, changelog, concepts,
configuration, notifications, keyboard-shortcuts, getting-started).

In `@web/app/`[locale]/docs/notifications/page.tsx:
- Around line 46-75: The table headers and cell labels (the <th>Variable</th>,
<th>Description</th> and the table rows showing
CMUX_NOTIFICATION_TITLE/SUBTITLE/BODY) plus the CodeBlock title="Examples" and
the OSC comparison rows (later in this file) are hard‑coded in English; replace
these literal strings with calls to the existing i18n/translation helper used in
this file (e.g., the getDictionary/t function or <Trans> wrapper used elsewhere
in page.tsx) so the table headers, cell descriptions, the CodeBlock title, and
the OSC comparison labels are rendered from the locale dictionary; update the
strings keys and add corresponding entries to the locale JSONs as needed and
ensure CodeBlock title prop receives the translated value (refer to the
CodeBlock component and the table markup in page.tsx to locate each place to
change).
- Around line 6-10: Replace the hardcoded export const metadata: Metadata with a
locale-aware metadata generator: implement an async generateMetadata({ params })
that reads the route locale (params.locale) and resolves the localized strings
for docs.notifications.title and docs.notifications.metaDescription (the same
keys used by useTranslations("docs.notifications")), then return a Metadata
object with those values; update any imports to use the server-side
i18n/translator utility you use for other routes so the browser title and
description are localized per-locale.

---

Duplicate comments:
In `@web/messages/da.json`:
- Around line 203-233: The Danish translation conflates the terms "pane" and
"panel" making both display as "Panel" and both IDs as "Panel-ID"; update the
entries to preserve the distinction by choosing distinct Danish terms (e.g.,
keep "pane" => "Panel" and translate "panel" => "Underpanel" or similar) and
adjust ID labels accordingly so "paneIdSocket" and "panelIdInternal" reflect
different names; modify the values for paneTitle, panelTitle, paneDesc,
panelDesc, paneNote, panelNote, paneIdSocket, and panelIdInternal to use the
distinct Danish words consistently across descriptions.
- Line 107: The Danish locale still contains English headings: update the locale
keys "blog.posts.zenOfCmux.title" and "wallOfLove.title" to Danish strings so
the views are fully localized; locate those keys in the translations file and
replace the English values ("The Zen of cmux" and the current "wallOfLove"
title) with appropriate Danish translations (e.g., translate to "Zen af cmux" or
another correct Danish phrasing) ensuring the JSON remains valid.

In `@web/messages/en.json`:
- Around line 109-110: Update the translation string for zenOfCmux.p1 by
replacing the incorrect phrase "hold their tools" with "use their tools" so the
value of the "p1" key reads: "cmux is not prescriptive about how developers use
their tools. It's a terminal and browser with a CLI, and the rest is up to you."
Locate the "p1" key under zenOfCmux (or the matching JSON entry containing that
sentence) and make the word substitution.

In `@web/messages/es.json`:
- Line 107: The Spanish messages file contains an untranslated title for
"zenOfCmux.title" — update the value for the "title" key under zenOfCmux in
web/messages/es.json to its Spanish equivalent (e.g., translate "The Zen of
cmux" into Spanish) so the title matches the existing Spanish summary/body;
ensure the JSON string value is properly escaped and preserves the key
"zenOfCmux.title".

In `@web/messages/ja.json`:
- Line 107: The "zenOfCmux" entry has its "title" still in English while
"summary" and "body" are Japanese; update the zenOfCmux -> title value in the
ja.json localization to a proper Japanese translation to avoid mixed-language
rendering and keep the key name "zenOfCmux" and "title" unchanged.

---

Nitpick comments:
In `@web/messages/it.json`:
- Around line 203-205: Summary: Use distinct Italian terms for the two concepts
so "Pane" and "Panel" don't collapse into the same word. Fix: in the JSON
entries related to the Pane concept (keys paneTitle, paneDesc, paneNote) replace
the current translation "Pannello" with a distinct Italian term (e.g., use
"Riquadro" or "Area del pannello" for Pane) and ensure all other keys that
represent the separate Panel concept keep "Pannello" (or another consistently
chosen term for Panel); update all occurrences of pane* and any other strings
referring to Pane across the file (also where panel strings appear) so the two
concepts use different, consistent Italian terms and preserve the original
placeholders like {right}, {down}, {nav}.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6d9422dc-903b-4e57-b253-96385d8eefe1

📥 Commits

Reviewing files that changed from the base of the PR and between 295c4f9 and a2231e0.

📒 Files selected for processing (21)
  • web/app/[locale]/docs/api/page.tsx
  • web/app/[locale]/docs/notifications/page.tsx
  • web/messages/ar.json
  • web/messages/bs.json
  • web/messages/da.json
  • web/messages/de.json
  • web/messages/en.json
  • web/messages/es.json
  • web/messages/fr.json
  • web/messages/it.json
  • web/messages/ja.json
  • web/messages/km.json
  • web/messages/ko.json
  • web/messages/no.json
  • web/messages/pl.json
  • web/messages/pt-BR.json
  • web/messages/ru.json
  • web/messages/th.json
  • web/messages/tr.json
  • web/messages/zh-CN.json
  • web/messages/zh-TW.json
✅ Files skipped from review due to trivial changes (2)
  • web/messages/zh-TW.json
  • web/messages/tr.json
🚧 Files skipped from review as they are similar to previous changes (10)
  • web/messages/bs.json
  • web/messages/de.json
  • web/messages/pl.json
  • web/messages/zh-CN.json
  • web/messages/km.json
  • web/messages/ko.json
  • web/messages/th.json
  • web/messages/ru.json
  • web/messages/fr.json
  • web/messages/ar.json

Comment thread web/messages/es.json Outdated
Comment on lines +203 to +233
"paneTitle": "Panel",
"paneDesc": "Una región dividida dentro de un workspace. Se crea dividiendo con {right} (derecha) o {down} (abajo). Navegue entre paneles con {nav} + teclas de flecha.",
"paneNote": "Cada panel puede contener múltiples superficies (pestañas dentro del panel).",
"surfaceTitle": "Superficie",
"surfaceDesc": "Una pestaña dentro de un panel. Cada panel tiene su propia barra de pestañas y puede contener múltiples superficies. Se crea con {new}, se navega con {prev} / {next} o {jump}.",
"surfaceNote": "Las superficies son las sesiones individuales de terminal o navegador con las que interactúa. Cada superficie tiene su propia variable de entorno CMUX_SURFACE_ID.",
"panelTitle": "Panel",
"panelDesc": "El contenido dentro de una superficie. Actualmente dos tipos:",
"panelTerminal": "Terminal: una sesión de terminal Ghostty",
"panelBrowser": "Navegador: una vista web integrada",
"panelNote": "Panel es principalmente un concepto interno. En la API de socket y la CLI, interactúa con superficies en lugar de directamente con paneles.",
"visualExample": "Ejemplo visual",
"visualExampleDesc": "En este ejemplo:",
"visualItem1": "La ventana contiene una barra lateral con tres workspaces (dev, server, logs)",
"visualItem2": "El workspace \"dev\" está seleccionado, mostrando dos paneles lado a lado",
"visualItem3": "El panel 1 tiene dos superficies ([S1] y [S2] en la barra de pestañas), con S1 activa",
"visualItem4": "El panel 2 tiene una superficie",
"visualItem5": "Cada superficie contiene un panel (una terminal en este caso)",
"summary": "Resumen",
"levelHeader": "Nivel",
"whatItIsHeader": "Qué es",
"createdByHeader": "Creado por",
"identifiedByHeader": "Identificado por",
"macosWindow": "Ventana de macOS",
"sidebarEntry": "Entrada en la barra lateral",
"splitRegion": "Región dividida",
"tabWithinPane": "Pestaña dentro del panel",
"terminalOrBrowser": "Terminal o navegador",
"automatic": "Automático",
"paneIdSocket": "ID de panel (API de socket)",
"panelIdInternal": "ID de panel (interno)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t collapse pane and panel into the same Spanish term.

paneTitle/panelTitle both become Panel, and paneIdSocket/panelIdInternal both become ID de panel. The concepts docs rely on those being separate hierarchy levels.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/es.json` around lines 203 - 233, The Spanish translations
collapse two distinct concepts—pane and panel—into the same words; update the
JSON so keys referencing the container region (paneTitle, paneDesc, paneNote,
paneIdSocket) use a distinct term (e.g., "Región" or "Panel dividido") and keys
referencing the content unit (panelTitle, panelDesc, panelTerminal,
panelBrowser, panelNote, panelIdInternal) use a different term (e.g., "Panel" or
"Contenido de panel"); ensure pane* and panel* keys use consistent, different
Spanish strings throughout so the hierarchy remains unambiguous.

Comment thread web/messages/es.json
"eula": "EULA"
},
"wallOfLove": {
"title": "Wall of Love",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize wallOfLove.title too.

This header is still English, so the Spanish page will ship mixed-language UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/es.json` at line 507, The JSON value for wallOfLove.title is
still in English; update the "wallOfLove.title" entry in web/messages/es.json to
its Spanish translation (e.g., "Muro de amor" or your approved localized string)
so the Spanish locale no longer mixes languages and matches other localized keys
like wallOfLove.*.

Comment thread web/messages/pt-BR.json Outdated
Comment on lines +209 to +233
"panelTitle": "Panel",
"panelDesc": "O conteúdo dentro de uma superfície. Atualmente dois tipos:",
"panelTerminal": "Terminal: uma sessão de terminal Ghostty",
"panelBrowser": "Navegador: uma visualização web integrada",
"panelNote": "Panel é majoritariamente um conceito interno. Na API de socket e CLI, você interage com superfícies em vez de panels diretamente.",
"visualExample": "Exemplo visual",
"visualExampleDesc": "Neste exemplo:",
"visualItem1": "A janela contém uma barra lateral com três workspaces (dev, server, logs)",
"visualItem2": "O workspace \"dev\" está selecionado, mostrando dois painéis lado a lado",
"visualItem3": "O Painel 1 tem duas superfícies ([S1] e [S2] na barra de abas), com S1 ativa",
"visualItem4": "O Painel 2 tem uma superfície",
"visualItem5": "Cada superfície contém um panel (um terminal neste caso)",
"summary": "Resumo",
"levelHeader": "Nível",
"whatItIsHeader": "O que é",
"createdByHeader": "Criado por",
"identifiedByHeader": "Identificado por",
"macosWindow": "Janela do macOS",
"sidebarEntry": "Entrada na barra lateral",
"splitRegion": "Região dividida",
"tabWithinPane": "Aba dentro do painel",
"terminalOrBrowser": "Terminal ou navegador",
"automatic": "Automático",
"paneIdSocket": "ID do painel (API de socket)",
"panelIdInternal": "ID do panel (interno)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Finish the pt-BR prose around the internal panel term.

If Panel is the chosen borrowed term to keep it distinct from Painel, the surrounding sentences still have English fragments (Panel is..., panels, ID do panel), so this section reads unfinished.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/messages/pt-BR.json` around lines 209 - 233, The Portuguese text mixes
English fragments for the borrowed term "Panel" and inconsistent uses of
"panel"/"painel"; update the strings to be fully natural Portuguese while
keeping "Panel" as the distinct borrowed term (and explain it once), by editing
keys like "panelTitle", "panelDesc", "panelTerminal", "panelBrowser",
"panelNote", "panelNote", "visualItem5", and "panelIdInternal" to (1) introduce
"Panel" as a borrowed term in Portuguese (e.g. "Panel (termo emprestado,
distinto de 'painel')"), (2) translate remaining English fragments and plurals
into Portuguese (e.g. replace "panels" with "Panels" or "Panel(s)" consistently
and render surrounding verbs/nouns in Portuguese), and (3) make
"panelIdInternal" a Portuguese label that clarifies it's an internal ID for a
Panel; ensure capitalization and pluralization of "Panel" are consistent across
these keys.

- 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

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/app/[locale]/docs/layout.tsx">

<violation number="1" location="web/app/[locale]/docs/layout.tsx:14">
P2: `docs.layoutTitle` is referenced in metadata but missing in 18 locale message files, so non-English docs titles are not reliably translated and may resolve to missing-message output.</violation>
</file>

<file name="web/app/[locale]/layout.tsx">

<violation number="1" location="web/app/[locale]/layout.tsx:32">
P2: `generateMetadata` now depends on `meta.*` translations that are missing for 18 locales, so non-English pages cannot produce localized metadata correctly.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const t = await getTranslations({ locale, namespace: "docs" });
return {
title: {
template: `%s — ${t("layoutTitle")}`,

@cubic-dev-ai cubic-dev-ai Bot Mar 12, 2026 •

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: docs.layoutTitle is referenced in metadata but missing in 18 locale message files, so non-English docs titles are not reliably translated and may resolve to missing-message output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/[locale]/docs/layout.tsx, line 14:

<comment>`docs.layoutTitle` is referenced in metadata but missing in 18 locale message files, so non-English docs titles are not reliably translated and may resolve to missing-message output.</comment>

<file context>
@@ -1,20 +1,28 @@
+  const t = await getTranslations({ locale, namespace: "docs" });
+  return {
+    title: {
+      template: `%s — ${t("layoutTitle")}`,
+      default: t("layoutTitle"),
+    },
</file context>
Fix with Cubic

params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "meta" });

@cubic-dev-ai cubic-dev-ai Bot Mar 12, 2026 •

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: generateMetadata now depends on meta.* translations that are missing for 18 locales, so non-English pages cannot produce localized metadata correctly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/[locale]/layout.tsx, line 32:

<comment>`generateMetadata` now depends on `meta.*` translations that are missing for 18 locales, so non-English pages cannot produce localized metadata correctly.</comment>

<file context>
@@ -19,40 +23,47 @@ const geistMono = Geist_Mono({
+  params: Promise<{ locale: string }>;
+}): Promise<Metadata> {
+  const { locale } = await params;
+  const t = await getTranslations({ locale, namespace: "meta" });
+  const url =
+    locale === "en" ? "https://cmux.dev" : `https://cmux.dev/${locale}`;
</file context>
Fix with Cubic

@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: d08c8e8bbe

ℹ️ 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".

params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "meta" });

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 Add missing locale metadata messages before reading meta

generateMetadata now reads meta.title/meta.description for every locale, but the non-English message bundles added in this commit (for example web/messages/ja.json and the other 17 localized files) do not define a top-level meta object or several *.metaTitle keys used across blog/docs/community pages. On localized routes this produces missing-message output (or intl errors, depending runtime config) in page titles/descriptions instead of translated metadata, which hurts SEO and localization quality.

Useful? React with 👍 / 👎.

- 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

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 19 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/messages/no.json">

<violation number="1" location="web/messages/no.json:186">
P2: Norwegian docs translations are missing required `metaTitle` keys for several docs namespaces that now call `t("metaTitle")` in metadata generation.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread web/messages/no.json

@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: f9f77acb2c

ℹ️ 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 on lines +10 to +11
title: t("metaTitle"),
description: t("metaDescription"),

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 Add localized wall-of-love metadata keys

generateMetadata always reads wallOfLove.metaTitle and wallOfLove.metaDescription, but the non-English message bundles in this commit only define wallOfLove.title/description (for example web/messages/ja.json), so localized /wall-of-love routes emit missing-message fallbacks (or throw under stricter intl settings) and lose proper translated SEO metadata.

Useful? React with 👍 / 👎.

@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: e6403898cd

ℹ️ 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 web/app/[locale]/page.tsx
<p className="text-muted">
{t.rich("faqNotificationsA", {
cliLink: (chunks) => (
<a href="/docs/notifications" className={linkClass}>

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 Preserve locale when linking to internal docs from home FAQ

These FAQ links use a raw <a href="/docs/..."> instead of the locale-aware navigation helper, so on localized pages they drop the explicit locale prefix and rely on cookie/Accept-Language detection. A user who lands directly on a non-English URL (e.g. shared /ja/... link) without a NEXT_LOCALE cookie can be sent back to English when clicking this link, which breaks locale continuity for core navigation.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen merged commit cf75da8 into main Mar 12, 2026
14 checks passed
@lawrencecchen
lawrencecchen deleted the task-i18n-website branch March 12, 2026 12:37
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…#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 <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
@coderabbitai coderabbitai Bot mentioned this pull request May 13, 2026

This branch was successfully deployed

1 active deployment
Preview — e6403898 Deployed Mar 12, 2026 by vercel[bot]
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.

1 participant