Skip to content

Conversation

@k0d13
Copy link
Owner

@k0d13 k0d13 commented Oct 13, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Added multi-language support with automatic locale detection and a language selector in the header.
    • Implemented locale-aware routing for all pages.
  • Improvements

    • Enhanced accessibility with improved screen reader support throughout the application.
    • Improved language detection with fallback support for unsupported languages.
    • Refined URL handling and redirects based on user locale preferences.

@vercel
Copy link

vercel bot commented Oct 13, 2025

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

Project Deployment Preview Comments Updated (UTC)
evaluate Ready Ready Preview Comment Oct 17, 2025 11:50pm

💡 Enable Vercel Agent with $100 free credit for automated AI reviews

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 13, 2025

Walkthrough

Integrates Sayable i18n framework across the website, replacing static text with locale-aware components. Adds locale-based routing via middleware, locale-specific metadata generation, and new configuration files. Updates package scripts and dependencies to support automated translation extraction and sitemap generation. Removes legacy error/not-found pages and runtime-based sitemap logic.

Changes

Cohort / File(s) Summary
Configuration & Build
.gitignore, apps/website/next.config.js, apps/website/package.json
Updated ignore patterns; replaced runtime-based redirects with locale-based patterns; added Sayable SWC plugin config; added build scripts (sayable extract/compile, sitemap generation); added dependencies: @sayable/*, next-sitemap, server-only
Sayable & Sitemap Configuration
apps/website/sayable.config.ts, apps/website/sitemap.config.ts
New Sayable config defining en locale, catalogues for ts/tsx files; new sitemap config using next-sitemap with locale-aware transforms and alternateRefs
i18n Infrastructure
apps/website/src/i18n.ts, apps/website/src/locales/en/messages.po, apps/website/src/middleware.ts
Created i18n module exporting Sayable instance with en loader; added English translation file with msgid/msgstr pairs; introduced middleware for locale detection from URL/cookies with redirect/rewrite logic
Layout & Metadata
apps/website/src/app/[locale]/layout.tsx, apps/website/src/app/[locale]/metadata.ts
Layout now async with generateStaticParams from say.locales, activates locale per route, sets lang/dir dynamically, wraps with BodyProviders/SayProvider; new generateBaseMetadata helper with locale/override support
Root-Level Metadata & Manifest
apps/website/src/app/manifest.ts, apps/website/src/app/metadata.ts (removed)
Manifest now async, uses say.load() for localized name/description/shortcuts; removed old root metadata module
Error & Not-Found Pages (Removed)
apps/website/src/app/error.tsx, apps/website/src/app/not-found.tsx, apps/website/src/app/sitemap.ts (removed)
Deleted legacy error and not-found UI components; removed runtime-based sitemap generation (manifest-read + fetchAllRuntimes logic replaced by configuration-driven approach)
Playgrounds Pages
apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/page.tsx, apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/not-found.tsx, apps/website/src/app/[locale]/(playgrounds)/playgrounds/page.tsx
Updated generateMetadata to accept locale in params, activate i18n before metadata generation; wrapped heading/content with Say components; not-found page now uses LocalisedLink and Say-wrapped text
Playground Cards & Listings
apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card-list.tsx, apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card.tsx
Integrated useSay hook, replaced hard-coded strings with Say templates for toast/placeholders/labels/sr-only text; changed search container to semantic div; card now uses LocalisedLink with Say-wrapped content
Product Pages
apps/website/src/app/[locale]/(products)/products/browser-extension/page.tsx, apps/website/src/app/[locale]/(products)/products/discord-bot/page.tsx
Wrapped h1, paragraphs, and button labels with Say components for localization
Editor Components
apps/website/src/components/editor/execute-bar/index.tsx, apps/website/src/components/editor/hooks.ts, apps/website/src/components/editor/opened-files/button.tsx
Added Say/useSay for dynamic placeholders and file-name localization (CLI Arguments, STDIN, Untitled); added fallback to textile language extension when unavailable
Explorer Components
apps/website/src/components/explorer/file/item.tsx, apps/website/src/components/explorer/index.tsx, apps/website/src/components/explorer/name.tsx, apps/website/src/components/explorer/use.tsx
Integrated Say for sr-only text, validation messages, and accessibility strings; updated toast messages to use say templates; replaced custom search element with semantic div
Language Detection & Localised Link
apps/website/src/components/languages.ts, apps/website/src/components/localised-link.tsx
Refactored detectLanguage to use pattern-based fallback; new LocalisedLink component prefixes href with locale when not default
Header & Footer
apps/website/src/components/header/index.tsx, apps/website/src/components/header/locale-switcher.tsx, apps/website/src/components/header/theme-switcher.tsx, apps/website/src/components/footer/index.tsx
Replaced next/link with LocalisedLink; wrapped navigation labels/icons with Say; added LocaleSwitcher component; switched width detection to useParams-based; updated sr-only text to use Say
Terminal & Utilities
apps/website/src/components/terminal/html-preview.tsx, apps/website/src/components/terminal/index.tsx, apps/website/src/components/providers.tsx
Wrapped fallback/status/instruction messages with Say components; BodyProviders now wraps children with SayProvider and accepts SayProvider props
Types & Removed Exports
apps/website/src/types.ts
Removed commented-out Next.js type aliases (ParamType, Params, PageProps, LayoutProps) from public API
Component Library
packages/components/package.json, packages/components/src/dropdown-menu.tsx
Added @radix-ui/react-dropdown-menu dependency; new dropdown-menu module with 14 exported Radix UI wrapper components (DropdownMenu, Trigger, Content, Item, CheckboxItem, RadioItem, etc.) with Tailwind styling and data-slot attributes

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant Middleware
    participant App
    participant i18n
    participant SayProvider

    Browser->>Middleware: Request /playgrounds or /en/playgrounds
    Middleware->>Middleware: Extract locale from path or cookie
    Middleware->>Middleware: Validate against i18n locales
    alt Locale in path
        Middleware-->>Browser: Rewrite/Redirect based on default
    else No locale, cookie exists
        Middleware->>Middleware: Prefix with cookie locale
        Middleware-->>Browser: Rewrite with locale prefix
    else No locale, no cookie
        Middleware->>Middleware: Use default locale
        Middleware-->>Browser: Rewrite with default locale
    end
    
    Browser->>App: Render /en/playgrounds
    App->>i18n: say.activate(locale)
    i18n->>i18n: Load locale-specific messages
    App->>SayProvider: Wrap children with Say context
    App->>App: Render Say-wrapped text components
    SayProvider->>SayProvider: Resolve text via active locale
    SayProvider-->>Browser: Render localized text
Loading
sequenceDiagram
    participant Component as React Component
    participant Say as Say Hook
    participant i18n as i18n Module
    participant Cache as Message Cache

    Component->>Say: useSay() called
    Say->>i18n: Get current Sayable instance
    i18n->>Cache: Retrieve active locale messages
    Cache-->>i18n: Return locale catalogue
    i18n-->>Say: Return say function
    Say-->>Component: Return template tag function
    Component->>Say: say`Localized Text`
    Say->>Cache: Lookup msgid in active locale
    Cache-->>Say: Return msgstr or msgid
    Say-->>Component: Return localized string
    Component->>Component: Render Say-wrapped JSX
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

The diff spans 40+ files with heterogeneous changes: new infrastructure (middleware, i18n setup, config files), systematic Say-wrapping across UI components (repetitive pattern but requires context verification), locale-aware metadata/routing logic updates, and removal of legacy runtime-based sitemap generation. While individual Say wrappings are mechanically simple, the breadth of affected files, new middleware/routing logic, configuration integration, and interdependencies between components and i18n require careful review to ensure correct locale propagation and no regressions.

Possibly related PRs

Poem

🐰 Hop, hop, hurrah! The website now speaks in many tongues,
Sayable magic weaves through every component spun.
Locales dance in the middleware's gentle flow,
As users switch languages—how delightfully they glow!
From en to wherever hearts roam free. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Add translations to website" accurately reflects the main objective of this changeset. The PR introduces comprehensive internationalization (i18n) infrastructure using the Sayable library, including the addition of a new English translation catalogue, locale-aware routing middleware, locale switcher components, and the integration of Say components throughout the website for localization support. The title is concise, specific, and clearly communicates that translations and i18n support are being added to the website application. A teammate scanning the git history would immediately understand this PR introduces translation capabilities.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch translate/website

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@k0d13
Copy link
Owner Author

k0d13 commented Oct 17, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 17, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot]
coderabbitai bot previously requested changes Oct 17, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

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

⚠️ Outside diff range comments (2)
apps/website/src/components/explorer/use.tsx (1)

63-71: Copying stale URL race; build URL first and handle clipboard errors.

setHash is async; copying location.href immediately can miss the new hash.

-  const saveAndCopyUrl = useCallback(
-    (e: Event) => {
-      e.preventDefault();
-      setHash(encodeExplorer(root));
-      navigator.clipboard.writeText(location.href);
-      toast.info(say`Saved and copied URL to clipboard`);
-    },
-    [say, setHash, root],
-  );
+  const saveAndCopyUrl = useCallback(
+    async (e: Event) => {
+      e.preventDefault();
+      const encoded = encodeExplorer(root);
+      // Update hash in state and copy the exact URL we intend to share.
+      setHash(encoded);
+      const url = new URL(window.location.href);
+      url.hash = encoded; // clears hash if encoded is ''
+      try {
+        await navigator.clipboard.writeText(url.toString());
+        toast.info(say`Saved and copied URL to clipboard`);
+      } catch {
+        toast.error(say`Could not copy URL to clipboard`);
+      }
+    },
+    [say, setHash, root],
+  );
apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card-list.tsx (1)

63-69: Bug: in‑place sort mutates input array (can mutate props).

Copy before sorting.

-  const sortedRuntimes = useMemo(() => {
-    return searchedRuntimes.sort((a, b) => {
+  const sortedRuntimes = useMemo(() => {
+    return [...searchedRuntimes].sort((a, b) => {
       if (sortBy === 'name') return a.name.localeCompare(b.name);
       return b.popularity - a.popularity;
     });
   }, [searchedRuntimes, sortBy]);
🧹 Nitpick comments (19)
apps/website/src/components/editor/execute-bar/index.tsx (1)

117-117: Consider localising the placeholder text.

The placeholder "Select an entry file..." is not wrapped with <Say>, which appears inconsistent with the PR's goal of adding translations to the website. User-facing placeholder text should be localised for comprehensive i18n coverage.

Apply this diff to localise the placeholder:

-            <SelectValue placeholder="Select an entry file..." />
+            <SelectValue placeholder={<Say>Select an entry file...</Say>} />

Note: Verify that the SelectValue component's placeholder prop accepts React nodes if this pattern is required.

apps/website/src/components/languages.ts (1)

165-165: Consider defensive handling for edge cases.

The non-null assertion on filename.split('.').reverse()[0]! assumes the filename always has at least one segment. While the default parameter handles empty strings safely (falling back to 'textile'), consider removing the assertion or adding explicit undefined handling for clarity.

Apply this diff for more explicit edge case handling:

 export function detectLanguage(filename = '') {
-  const ext = filename.split('.').reverse()[0]!;
+  const ext = filename.split('.').reverse()[0] ?? '';
   if (ext in LANGUAGE_PATTERNS) return ext;
apps/website/src/middleware.ts (1)

7-7: Consider defensive handling for empty locales array.

The code assumes say.locales[0] exists with a non-null assertion. If the i18n configuration is incomplete or misconfigured, this could cause a runtime error. Consider adding a fallback or validation.

Apply this diff to add a fallback:

-  const defaultLocale = say.locales[0]!;
+  const defaultLocale = say.locales[0] ?? 'en';
apps/website/src/components/explorer/use.tsx (1)

39-40: Avoid “as” cast; prefer inferred/generic typing.

If useSay already returns Sayable, drop the cast; otherwise use generics for clarity.

- const say = useSay() as Sayable;
+ const say = useSay() as Sayable; // If useSay<Sayable>() is supported:
+ // const say = useSay<Sayable>();
apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card.tsx (1)

82-85: Optional: use aria-label on the link instead of an sr-only span.

Keeps DOM flatter and remains accessible.

- <span className="sr-only">
-   <Say>Open {runtime.name} Playground</Say>
- </span>
+ <span className="sr-only">
+   <Say>Open {runtime.name} Playground</Say>
+ </span>
+ // Alternatively:
+ // <LocalisedLink aria-label={say`Open ${runtime.name} Playground`} ... />
apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card-list.tsx (3)

87-97: Use a standard container for the search landmark + add an accessible label.

Replace non‑standard tag; ensure the input has an aria‑label.

- <search className="flex gap-3">
+ <div role="search" className="flex gap-3">
...
- </search>
+ </div>

And label the input:

  <Input
    className="absolute inset-0 h-full w-full pl-7"
-   placeholder={say`Search runtime playgrounds...`}
+   placeholder={say`Search runtime playgrounds...`}
+   aria-label={say`Search runtime playgrounds...`}
    value={search ?? ''}
    onChange={(e) => setSearch(e.target.value)}
  />

Also applies to: 134-134


72-75: Drop non‑null assertion; use a type‑guarding filter.

Safer typing when ids go stale.

-  return pinnedRuntimeIds
-    .map((id) => initialRuntimes.find((r) => r.id === id)!)
-    .filter(Boolean);
+  return pinnedRuntimeIds
+    .map((id) => initialRuntimes.find((r) => r.id === id))
+    .filter((r): r is Runtime => Boolean(r));

79-84: Optional: prevent duplicate toasts on locale toggles.

The effect depends on say; consider showing once per mount or guarding with a ref.

-  useEffect(() => {
-    if (hash)
-      toast.info(say`Choose a playground!`, { icon: <CircleDotIcon className="size-4" /> });
-  }, [say, hash]);
+  useEffect(() => {
+    if (!hash) return;
+    let shown = false;
+    if (!shown) {
+      shown = true;
+      toast.info(say`Choose a playground!`, { icon: <CircleDotIcon className="size-4" /> });
+    }
+  }, [hash, say]);
apps/website/src/i18n.ts (1)

4-11: Type-safety and future‑proofing of loaders.

Consider constraining the loaders’ return type and freezing locale keys to catch mistakes at build time.

Example:

-import { Sayable } from 'sayable';
+import { Sayable } from 'sayable';
+type Messages = Record<string, string>;
 
-export default new Sayable({
-  en: () =>
+export default new Sayable({
+  en: () =>
     import('./locales/en/messages.json', { with: { type: 'json' } }) //
-      .then((m) => m.default),
-  fr: () =>
+      .then((m) => m.default as Messages),
+  fr: () =>
     import('./locales/fr/messages.json', { with: { type: 'json' } }) //
-      .then((m) => m.default),
+      .then((m) => m.default as Messages),
-});
+} as const);
apps/website/src/components/terminal/index.tsx (2)

103-121: Minor conditional simplification (optional).

Inside a branch guarded by !result?.run.output, the nested condition result && !result.run.output is redundant. Simplify for readability.

-                ) : result && !result.run.output ? (
+                ) : result ? (

45-47: Prefer CSS spacing over &nbsp; (optional).

Use Tailwind spacing (e.g., gap-x-2 or ml-1) instead of &nbsp; to avoid layout quirks and improve RTL handling.

Also applies to: 60-62, 75-77

apps/website/src/locales/fr/messages.po (1)

38-41: Terminology consistency (optional).

Consider “environnement d’exécution” instead of “runtime” for FR locale to match other entries already using that phrasing.

Also applies to: 50-53

apps/website/src/app/[locale]/metadata.ts (2)

30-38: Derive OpenGraph titles as strings and exclude current locale from alternates.

metadata.title can be a string or a template object. Coerce to string for OG/Twitter. Also, alternateLocale should omit the current locale.

   return {
     metadataBase: new URL(env.WEBSITE_URL),
     ...metadata,
     openGraph: {
       type: 'website',
-      title: metadata.title,
-      description: metadata.description,
-      locale: say.locale,
-      alternateLocale: say.locales,
-      siteName: metadata.title,
-      url: new URL(`/${say.locale}${pathname}`, env.WEBSITE_URL),
+      title: typeof metadata.title === 'string' ? metadata.title : metadata.title?.default ?? 'Evaluate',
+      description: metadata.description ?? '',
+      locale: say.locale,
+      alternateLocale: say.locales.filter((l) => l !== say.locale),
+      siteName: typeof metadata.title === 'string' ? metadata.title : metadata.title?.default ?? 'Evaluate',
+      url: new URL(`/${say.locale}${pathname}`, env.WEBSITE_URL),
     },
     twitter: {
       card: 'summary',
-      title: metadata.title,
-      description: metadata.description,
+      title: typeof metadata.title === 'string' ? metadata.title : metadata.title?.default ?? 'Evaluate',
+      description: metadata.description ?? '',
       site: '@evaluatedotrun',
       creator: '@apteryxxyz',
     },

6-25: Allow async overrides (optional).

Some callers may need async data to build metadata. Consider accepting overrides returning Promise<Metadata>.

-export async function generateBaseMetadata(
+export async function generateBaseMetadata(
   say: Sayable,
   pathname: `/${string}`,
-  overrides?: (say: Sayable) => Metadata,
+  overrides?: (say: Sayable) => Metadata | Promise<Metadata>,
 ) {
   await say.load();
-  const metadata = merge(
+  const metadata = merge(
     {
       title: 'Evaluate',
       description: say`Test code in any programming language with Evaluate! Run code snippets in your browser, without installing anything.`,
       keywords: [
         say`online playground`,
         say`code playground`,
         say`code sandbox`,
         say`online compiler`,
         say`online interpreter`,
       ],
     } satisfies Metadata,
-    overrides?.(say) ?? {},
+    (await overrides?.(say)) ?? {},
   );
apps/website/src/components/header/locale-switcher.tsx (1)

29-39: Optionally hide draft locales from the menu.

If FR is not ready, filter it out here until reviewed, or drive via a flag.

-        {say.locales.map((locale) => (
+        {say.locales
+          .filter((l) => process.env.NEXT_PUBLIC_ENABLE_FR !== 'false' || l !== 'fr')
+          .map((locale) => (
             <DropdownMenuItem
               key={locale}
               className="capitalize cursor-pointer"
               asChild
             >
               <LocaleSwitcherItem locale={locale} />
             </DropdownMenuItem>
-        ))}
+        ))}
apps/website/src/app/[locale]/layout.tsx (1)

28-28: Add dir for RTL locales.

Improves accessibility and layout for RTL languages.

Apply:

-      <html key="html" lang={say.locale} className="dark">
+      <html
+        key="html"
+        lang={say.locale}
+        dir={['ar', 'fa', 'he', 'ur', 'ps'].includes(say.locale) ? 'rtl' : 'ltr'}
+        className="dark"
+      >
apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/page.tsx (3)

22-22: Drop unnecessary await on props.params

Unless props.params is intentionally a Promise in your PageProps, the await is redundant and can confuse types. Prefer direct destructuring.

-  const { locale, playground } = await props.params;
+  const { locale, playground } = props.params;
@@
-  const { playground } = await props.params;
+  const { playground } = props.params;

Confirm whether PageProps['params'] is Promise-like. If it is, consider documenting that convention in ~/types to avoid repeated questions.

Also applies to: 40-40


28-28: Add a redirect from old path to new pluralised path

You switched to /playgrounds/:id. Keep backlinks alive with a permanent redirect.

Example (next.config.mjs):

export default {
  async redirects() {
    return [
      { source: '/playground/:slug', destination: '/playgrounds/:slug', permanent: true },
    ];
  },
};

41-41: Avoid double fetch of the same runtime

You fetch the runtime in both generateMetadata and the page. Consider request‑scope memoisation.

Example:

// at top
import { cache } from 'react';

const getRuntime = cache(async (id: string) =>
  fetchRuntimeById(decodeURIComponent(id)),
);

// usage in both places
const runtime = await getRuntime(playground);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad3d24c and 75b7f23.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (42)
  • .gitignore (1 hunks)
  • apps/website/next.config.js (2 hunks)
  • apps/website/package.json (4 hunks)
  • apps/website/sayable.config.ts (1 hunks)
  • apps/website/sitemap.config.ts (1 hunks)
  • apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/not-found.tsx (1 hunks)
  • apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/page.tsx (2 hunks)
  • apps/website/src/app/[locale]/(playgrounds)/playgrounds/page.tsx (2 hunks)
  • apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card-list.tsx (5 hunks)
  • apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card.tsx (2 hunks)
  • apps/website/src/app/[locale]/(products)/products/browser-extension/page.tsx (4 hunks)
  • apps/website/src/app/[locale]/(products)/products/discord-bot/page.tsx (2 hunks)
  • apps/website/src/app/[locale]/layout.tsx (2 hunks)
  • apps/website/src/app/[locale]/metadata.ts (1 hunks)
  • apps/website/src/app/error.tsx (0 hunks)
  • apps/website/src/app/manifest.ts (1 hunks)
  • apps/website/src/app/metadata.ts (0 hunks)
  • apps/website/src/app/not-found.tsx (0 hunks)
  • apps/website/src/app/sitemap.ts (0 hunks)
  • apps/website/src/components/editor/execute-bar/index.tsx (3 hunks)
  • apps/website/src/components/editor/hooks.ts (1 hunks)
  • apps/website/src/components/editor/opened-files/button.tsx (4 hunks)
  • apps/website/src/components/explorer/file/item.tsx (3 hunks)
  • apps/website/src/components/explorer/index.tsx (4 hunks)
  • apps/website/src/components/explorer/name.tsx (4 hunks)
  • apps/website/src/components/explorer/use.tsx (4 hunks)
  • apps/website/src/components/footer/index.tsx (2 hunks)
  • apps/website/src/components/header/index.tsx (6 hunks)
  • apps/website/src/components/header/locale-switcher.tsx (1 hunks)
  • apps/website/src/components/header/theme-switcher.tsx (2 hunks)
  • apps/website/src/components/languages.ts (1 hunks)
  • apps/website/src/components/localised-link.tsx (1 hunks)
  • apps/website/src/components/providers.tsx (2 hunks)
  • apps/website/src/components/terminal/html-preview.tsx (2 hunks)
  • apps/website/src/components/terminal/index.tsx (9 hunks)
  • apps/website/src/i18n.ts (1 hunks)
  • apps/website/src/locales/en/messages.po (1 hunks)
  • apps/website/src/locales/fr/messages.po (1 hunks)
  • apps/website/src/middleware.ts (1 hunks)
  • apps/website/src/types.ts (0 hunks)
  • packages/components/package.json (1 hunks)
  • packages/components/src/dropdown-menu.tsx (1 hunks)
💤 Files with no reviewable changes (5)
  • apps/website/src/app/error.tsx
  • apps/website/src/app/metadata.ts
  • apps/website/src/types.ts
  • apps/website/src/app/sitemap.ts
  • apps/website/src/app/not-found.tsx
🔇 Additional comments (42)
apps/website/src/components/explorer/file/item.tsx (2)

4-4: LGTM: Say import added for internationalization.

The import is correctly added and used to wrap accessibility labels below.


64-66: LGTM: Accessibility labels now translatable with confirmed pattern.

Wrapping the screen reader-only text in Say components enables these accessibility labels to be translated. This pattern is confirmed as standard across the codebase—used consistently in playground cards, terminal, and other components for translatable i18n accessibility labels. The implementation is correct.

Please verify at runtime that screen readers properly announce the translated accessibility text in your target browsers and languages. You may test this by enabling a screen reader (e.g., NVDA, JAWS, or VoiceOver) and confirming the translated "Rename File" and "Delete File" labels are announced correctly when navigating the icon buttons.

Also applies to: 80-82

packages/components/src/dropdown-menu.tsx (8)

1-1: LGTM!

The 'use client' directive is correctly placed for this client-side component module.


3-5: LGTM!

Imports are well-organised and include all necessary dependencies.


60-81: LGTM!

The DropdownMenuItem component correctly implements variant handling ('default' | 'destructive') and inset support. The Tailwind classes appropriately style focus states, disabled states, and icon sizing.


83-107: LGTM!

The DropdownMenuCheckboxItem component correctly positions the check indicator and handles the checked state. The absolute positioning and icon sizing are appropriate.


109-142: LGTM!

The DropdownMenuRadioGroup and DropdownMenuRadioItem components follow the same solid pattern as the checkbox variants. The filled circle indicator is appropriately sized and positioned.


144-191: LGTM!

The DropdownMenuLabel, DropdownMenuSeparator, and DropdownMenuShortcut components are correctly implemented. The use of a native span for the shortcut is appropriate since it's purely presentational.


193-237: LGTM!

The sub-menu components are well-implemented. The DropdownMenuSubTrigger appropriately includes the chevron indicator, and the DropdownMenuSubContent styling is consistent with the main content component.


239-255: LGTM!

All components are correctly exported with clear, consistent naming.

packages/components/package.json (1)

21-21: No issues found – dependency version is current and secure.

Version 2.1.16 is the latest version available on npm, and no security vulnerabilities are reported for this package. The dependency specification is appropriate and requires no changes.

apps/website/src/components/editor/execute-bar/index.tsx (3)

17-17: LGTM!

The import is correctly added and positioned alphabetically.


132-132: LGTM!

Appropriately wrapping the user-facing text for internationalization.


148-152: Verify the @sayable/react pattern with mixed visible and screen-reader-only content.

The Say component wraps three spans together (sr-only "Execute", visible runtime name, sr-only "Code"). Since @sayable/react is an early-stage alpha library (0.0.0-alpha.8) with limited public documentation, the behaviour when rendering mixed visible and hidden content is unclear. The concern that the component may vocalise the entire block as a single translation unit—potentially interfering with intended screen reader announcements—is reasonable.

This is the only instance of this pattern in the codebase, which suggests it may warrant closer inspection. Confirm that this produces the correct screen reader output ("Execute [runtime name] Code") and that localisation works correctly with the mixed-content structure.

.gitignore (1)

9-10: LGTM! Appropriate exclusions for generated artefacts.

Both messages.json (i18n catalogue) and sitemap.xml (generated during postbuild) are correctly excluded from version control as build-time artefacts.

apps/website/src/components/terminal/html-preview.tsx (1)

3-3: LGTM! Consistent Sayable integration for fallback text.

The fallback message is now wrapped with Say for localization, aligning with the broader i18n strategy across the codebase.

Also applies to: 40-40

apps/website/src/components/header/theme-switcher.tsx (1)

4-4: LGTM! Accessible, translatable label.

The screen-reader text is now wrapped with Say, ensuring the theme toggle button is accessible across locales.

Also applies to: 25-27

apps/website/next.config.js (2)

53-55: LGTM! SWC plugin correctly configured for Sayable.

The experimental SWC plugin enables compile-time i18n message extraction for Sayable, which is essential for the translation workflow.


16-20: Locale pattern consistency verified; clarify intent of permanent redirect change.

The middleware LOCALE regex /^(?:[a-zA-Z]{2})(?:-[a-zA-Z]{2})?$/ and the next.config.js pattern (?:[a-zA-Z]{2})(?:-[a-zA-Z]{2})? are consistent—both enforce the same two-letter language code with optional two-letter region code.

However, confirm that changing permanent: false is intentional. This affects caching behaviour; temporary redirects (307/308) are cached differently than permanent ones (301/308), which may impact SEO and user experience.

apps/website/src/middleware.ts (3)

14-36: LGTM! Locale routing logic is well-structured.

The middleware correctly handles three routing scenarios:

  1. Strips default locale from path (cleaner URLs)
  2. Applies cookie-based locale preference with appropriate redirect/rewrite
  3. Falls back to default locale rewrite

The use of rewrite vs. redirect is appropriate for hiding/showing locale changes to users.


48-52: LGTM! Comprehensive matcher pattern.

The matcher correctly excludes Next.js internals, static assets, and common file types, ensuring the middleware only runs on actual page routes.


54-60: LGTM! Locale extraction logic is sound.

The helper function correctly extracts the first path segment and validates it against the locale pattern. The partIndex * 2 + 1 calculation properly accounts for the leading empty string in the split array.

apps/website/src/components/footer/index.tsx (1)

5-5: LGTM! Footer links now locale-aware.

Migrating from Link to LocalisedLink ensures the footer navigation participates in the locale routing system, maintaining consistency with the rest of the site.

Also applies to: 21-33

apps/website/src/app/[locale]/(products)/products/browser-extension/page.tsx (1)

2-2: LGTM! Comprehensive Sayable integration for product page.

All user-facing text (heading, description, and button labels) is now wrapped with Say components, enabling full localization support for the browser extension product page.

Also applies to: 11-21, 42-44, 128-130

apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card.tsx (1)

76-86: LGTM — correct locale‑aware overlay link with accessible label.

apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/not-found.tsx (1)

10-21: LGTM — consistent localisation and locale‑aware navigation.

apps/website/src/components/explorer/index.tsx (2)

44-44: Well-structured accessibility and i18n implementation.

The integration of Say components for both visible text and screen-reader content ensures proper localization throughout the UI. The use of template literals for button titles provides good UX for tooltips.

Also applies to: 50-85


108-110: Good user experience with localized empty state.

Wrapping the helper text in a Say component ensures the empty-state message is properly translated.

apps/website/src/components/explorer/name.tsx (2)

35-43: Well-implemented validation message localization.

All error messages are properly wrapped with say template literals, ensuring they're translatable. The inclusion of say in the dependency array at line 43 follows the same pattern as other files; refer to the earlier comment about verifying say's stability.


80-84: Consistent special-file naming with localization.

The mapping of special file names (::args::, ::input::) to localized strings is implemented consistently with the rest of the codebase.

apps/website/src/components/header/index.tsx (3)

33-72: Excellent integration of locale-aware navigation and i18n.

The replacement of Link with LocalisedLink throughout the navigation ensures proper locale handling, and wrapping all text in Say components provides comprehensive translation coverage.


97-97: Good addition of locale switcher for user control.

The LocaleSwitcher component provides users with the ability to change their language preference directly from the header.


28-28: Verification confirms the route structure and logic are correct.

The route structure supports the playground parameter check:

  • Route src/app/[locale]/(editor)/playgrounds/[playground]/ exists with the [playground] dynamic segment
  • useParams() returns {locale, playground} only on this editor route
  • On the /playgrounds list page, useParams() returns only {locale}, so the check correctly returns false

The implementation correctly identifies playground pages without false-positives:

  • Header applies full width only when navigating to /playgrounds/[id] (editor pages)
  • Header applies constrained width on /playgrounds list and all other pages
  • This is the intended behaviour based on the route structure
apps/website/sayable.config.ts (1)

1-12: Standard Sayable configuration structure.

The configuration properly defines:

  • Source locale as English
  • French as the additional locale
  • Appropriate catalogue output pattern
apps/website/src/app/[locale]/(products)/products/discord-bot/page.tsx (1)

10-20: Proper localization of page content.

All user-facing text is appropriately wrapped in Say components for translation, maintaining the existing page structure.

Also applies to: 37-39

apps/website/src/components/providers.tsx (1)

34-36: Props properly forwarded from parent component—verification complete.

The BodyProviders function receives props correctly from src/app/[locale]/layout.tsx via <BodyProviders {...say}>, where say is the Sayable instance imported from ~/i18n. The spread operator correctly forwards the i18n object containing locale and messages data to SayProvider. The integration follows React best practices and is functioning as intended.

apps/website/package.json (1)

24-24: Review the stability and registry source of @sayable packages before production deployment.

Based on web searches, @sayable packages do not appear on the public npm registry or within widely documented React component libraries. The packages listed (@sayable/react v0.0.0-alpha.8 at lines 24, 52, 64-67, 72) may be internal/private packages, local monorepo references, or managed through a custom registry.

To clarify the production-readiness status:

  • Confirm whether @sayable packages are published via a private registry or workspace configuration
  • If internal, document the versioning strategy and stability guarantees for these alpha versions
  • If external but not yet stable, consider whether production deployment is appropriate given the pre-release status

Verify registry configuration (.npmrc, monorepo workspace settings, or custom registries) to understand the source and maturity of these dependencies before proceeding to production.

apps/website/src/components/editor/opened-files/button.tsx (1)

40-44: Verify that say from @sayable/react returns a stable reference across renders and remove redundant fallback.

Including say in the dependency array is only correct if useSay() returns a stable reference—otherwise useMemo will recompute name on every render, defeating its purpose. Since @sayable/react is at version 0.0.0-alpha.8, you'll need to verify the hook implementation (ideally wrapped in useCallback or useMemo).

Additionally, line 56 has a redundant fallback: sayUntitled`` is already provided on line 43, so remove it from the fallback span.

apps/website/src/app/[locale]/metadata.ts (1)

46-53: Canonical URL: consider absolute or locale‑aware canonical.

Current canonical is the non‑prefixed pathname. For locale pages, you may want either the locale‑prefixed canonical, or an absolute URL. Validate with your SEO strategy.

apps/website/src/i18n.ts (1)

6-10: No action required—JSON import attributes are properly supported.

The codebase configuration fully supports the { with: { type: 'json' } } syntax used in i18n.ts:

  • TypeScript 5.8.3 supports import attributes (TC39 Stage 3 feature)
  • Next.js 13+ with SWC supports import attributes natively
  • resolveJsonModule: true is enabled in tsconfig.base.json
  • Code uses the correct, modern with: syntax (not the deprecated assert: syntax)

The implementation follows current standards and best practices.

apps/website/src/components/localised-link.tsx (1)

6-13: Verify and clarify intent for locale parameter and UrlObject support.

The review identifies real but currently unused edge cases. All observed LocalisedLink usages pass string hrefs—no UrlObject instances found. The codebase currently works correctly for all cases (including anchor links like #section). However:

  • The locale prop is genuinely unused (present in type signature but not destructured; passed via ...props to Link instead).
  • If future code passes UrlObject or external links to LocalisedLink, the current string concatenation approach would break.

The suggested refactor is comprehensive but defensive for patterns not yet in the codebase. Verify whether you intend to support UrlObject or handle the unused locale parameter, then decide if the refactor is necessary or if a simpler fix (removing unused locale from the type signature) suffices.

apps/website/src/app/[locale]/(playgrounds)/playgrounds/page.tsx (1)

8-11: Awaiting props.params is required in Next.js 13+ App Router—this review misunderstands the framework semantics.

In Next.js App Router, params and searchParams are Promises that must be awaited before use. The current code correctly does const { locale } = await props.params;. The proposed change to destructure { params } and then access const { locale } = params without awaiting would fail, since params itself remains a Promise and cannot be destructured directly.

Whilst the review's suggestion to call await say.load(locale) before say.activate(locale) aligns with the pattern seen in layout.tsx, this is not a critical issue since generateBaseMetadata already calls await say.load() internally. The current implementation works because say is a global instance—activating the locale first and then loading will load for the active locale.

Additionally, both existing generateMetadata implementations in the codebase use the same pattern of awaiting props.params, confirming this is the correct approach.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (1)
apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card-list.tsx (1)

98-107: Make the clear control keyboard-accessible.

An icon with role="button" isn’t focusable or operable via keyboard. Use a real button (or add tabIndex and key handlers).

-          {search && (
-            <XIcon
-              role="button"
-              className="absolute top-[27%] right-2 size-4 cursor-pointer opacity-50"
-              onClick={() => setSearch('')}
-            >
-              <span className="sr-only">
-                <Say>Clear Search</Say>
-              </span>
-            </XIcon>
-          )}
+          {search && (
+            <button
+              type="button"
+              aria-label={say`Clear Search`}
+              className="absolute top-[27%] right-2 inline-flex items-center justify-center size-5 cursor-pointer opacity-50"
+              onClick={() => setSearch('')}
+            >
+              <XIcon className="size-4" aria-hidden />
+            </button>
+          )}
♻️ Duplicate comments (2)
apps/website/src/components/terminal/index.tsx (1)

30-33: Previous console.log concern appears resolved.

No stray console.log(result) in this area anymore. Thanks for tidying.

apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/page.tsx (1)

32-35: Dedupe keywords, use locale-aware lowercasing, and guard nulls.

Improves SEO hygiene and avoids runtime faults if aliases/tags are absent.

-      keywords: [runtime.name, ...runtime.aliases, ...runtime.tags].map((k) =>
-        k.toLowerCase(),
-      ),
+      keywords: Array.from(
+        new Set(
+          [runtime.name, ...(runtime.aliases ?? []), ...(runtime.tags ?? [])]
+            .map((k) => String(k).toLocaleLowerCase(locale)),
+        ),
+      ),
🧹 Nitpick comments (6)
apps/website/src/components/terminal/index.tsx (3)

42-46: Replace non-breaking spaces with CSS spacing and mark decorative icons aria-hidden.

Using   couples layout to content and can break RTL/localisation. Prefer CSS gaps/margins; also ensure decorative icons are hidden from AT.

Apply:

-                <CodeIcon className="size-4" />
-                <span>
-                  &nbsp;<Say>Run</Say>
-                </span>
+                <CodeIcon className="size-4" aria-hidden="true" />
+                <span className="ml-2">
+                  <Say>Run</Say>
+                </span>

-                <PackageCheckIcon className="size-4" />
-                <span>
-                  &nbsp;<Say>Compile</Say>
-                </span>
+                <PackageCheckIcon className="size-4" aria-hidden="true" />
+                <span className="ml-2">
+                  <Say>Compile</Say>
+                </span>

-                <FullscreenIcon className="size-4" />
-                <span>
-                  &nbsp;<Say>Preview</Say>
-                </span>
+                <FullscreenIcon className="size-4" aria-hidden="true" />
+                <span className="ml-2">
+                  <Say>Preview</Say>
+                </span>

-              <Trash2Icon className="size-4" />
+              <Trash2Icon className="size-4" aria-hidden="true" />

Also applies to: 57-61, 72-76, 86-90


99-100: Announce state messages to screen readers.

Empty-state/status overlays should be announced. Add a polite live region.

Apply:

-            <div className="absolute inset-0 flex items-center justify-center bg-black/40">
+            <div
+              className="absolute inset-0 flex items-center justify-center bg-black/40"
+              role="status"
+              aria-live="polite"
+            >

Repeat for the compile overlay.

Also applies to: 129-130


112-117: Icon inside translation: confirm Say supports React-node interpolation.

You’re rendering inside . If Say doesn’t support element interpolation, extraction may fail or translators won’t see placeholders. At minimum, hide the icon from AT.

  • Confirm Say’s support for nested elements/ICU-like placeholders.
  • If unsupported, split the sentence or use Say’s placeholder/components API (if available).

Apply minimal a11y fix:

-                    <PlayIcon size={16} className="inline"></PlayIcon> to
+                    <PlayIcon size={16} className="inline" aria-hidden="true" /> to

If interpolation isn’t supported, one safe fallback:

-                  <Say>
-                    Write your code and press
-                    <PlayIcon size={16} className="inline"></PlayIcon> to
-                    execute it. The results will be displayed here.
-                  </Say>
+                  <span className="inline-flex items-center gap-1">
+                    <Say>Write your code and press</Say>
+                    <PlayIcon size={16} aria-hidden="true" />
+                    <Say>to execute it. The results will be displayed here.</Say>
+                  </span>
apps/website/src/components/editor/execute-bar/index.tsx (1)

149-153: Avoid wrapping dynamic content in a single ; prefer an aria-label template.

Including {runtime.name} inside can explode your catalogue (one key per runtime) and may confuse extractors. Use an aria-label with a template, and keep the visible runtime name outside the translation.

Apply:

-        <Button
+        <Button
           type="submit"
           className="aspect-square p-0 sm:aspect-auto sm:px-4 sm:py-2"
           disabled={isPending}
+          aria-label={say`Execute ${runtime.name} Code`}
         >
@@
-          <Say>
-            <span className="sr-only">Execute</span>
-            <span className="hidden sm:block">{runtime.name}</span>
-            <span className="sr-only">Code</span>
-          </Say>
+          <span className="hidden sm:block" aria-hidden>
+            {runtime.name}
+          </span>
apps/website/src/components/explorer/index.tsx (1)

94-96: Replace as never cast; provide a typed adapter.

ref={dropzoneRef as never} hides a type mismatch. Prefer aligning types at the hook boundary or adapting via a small wrapper to the expected ref type.

Example adapter:

-        <ScrollArea
-          // TODO: Fix types
-          ref={dropzoneRef as never}
+        <ScrollArea
+          // Adapter to satisfy ScrollArea's ref type without using `as never`
+          ref={(el) => (dropzoneRef as unknown as (n: HTMLDivElement | null) => void)?.(el)}

Better: update useDropzone() to return a RefCallback or MutableRefObject<HTMLDivElement | null> to match ScrollArea.

apps/website/src/app/manifest.ts (1)

14-27: Spec-compliant now; consider adding display: 'standalone'.

You’ve avoided leaking url into the root (good). To improve the PWA UX, add display: 'standalone' (commonly recommended).

   return {
     name: manifest!.name,
     short_name: manifest!.short_name,
     description: manifest!.description,
+    display: 'standalone',
     theme_color: '#2fc186',
     background_color: '#ffffff',
     scope: '/',
     start_url: '/',
@@
     shortcuts,
   };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75b7f23 and e3976d8.

📒 Files selected for processing (15)
  • apps/website/sayable.config.ts (1 hunks)
  • apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/page.tsx (2 hunks)
  • apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card-list.tsx (5 hunks)
  • apps/website/src/app/[locale]/layout.tsx (2 hunks)
  • apps/website/src/app/manifest.ts (1 hunks)
  • apps/website/src/components/editor/execute-bar/index.tsx (5 hunks)
  • apps/website/src/components/editor/opened-files/button.tsx (4 hunks)
  • apps/website/src/components/explorer/index.tsx (5 hunks)
  • apps/website/src/components/explorer/name.tsx (4 hunks)
  • apps/website/src/components/explorer/use.tsx (3 hunks)
  • apps/website/src/components/header/locale-switcher.tsx (1 hunks)
  • apps/website/src/components/terminal/index.tsx (8 hunks)
  • apps/website/src/i18n.ts (1 hunks)
  • apps/website/src/locales/en/messages.po (1 hunks)
  • apps/website/src/middleware.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/website/src/components/explorer/name.tsx
  • apps/website/src/components/header/locale-switcher.tsx
  • apps/website/src/middleware.ts
  • apps/website/src/components/editor/opened-files/button.tsx
  • apps/website/src/locales/en/messages.po
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-17T15:37:26.497Z
Learnt from: apteryxxyz
PR: apteryxxyz/Evaluate#99
File: apps/website/src/app/[locale]/layout.tsx:17-24
Timestamp: 2025-10-17T15:37:26.497Z
Learning: In Next.js 15, the params property in layout and page components is a Promise and must be awaited in server components (e.g., `const { locale } = await props.params;`) or unwrapped with React's `use()` in client components.

Applied to files:

  • apps/website/src/app/[locale]/layout.tsx
📚 Learning: 2025-10-17T15:34:13.400Z
Learnt from: apteryxxyz
PR: apteryxxyz/Evaluate#99
File: apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/page.tsx:29-35
Timestamp: 2025-10-17T15:34:13.400Z
Learning: In the Evaluate website's i18n system (using Sayable), the `say` identifier in callbacks must not be renamed. Code transformers specifically look for `say` identifiers to perform compile-time/build-time transformations. This applies to all i18n builder callbacks, such as those passed to `generateBaseMetadata`.

Applied to files:

  • apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/page.tsx
🧬 Code graph analysis (5)
apps/website/src/components/explorer/index.tsx (1)
apps/website/src/components/explorer/folder/item.tsx (1)
  • ExplorerFolderItem (32-115)
apps/website/src/app/[locale]/layout.tsx (2)
apps/website/src/types.ts (1)
  • LayoutProps (35-41)
apps/website/src/components/providers.tsx (1)
  • BodyProviders (34-69)
apps/website/src/app/manifest.ts (2)
apps/website/src/app/sitemap.ts (4)
  • RoutesManifest (7-10)
  • loadStaticPaths (12-24)
  • loadDynamicPaths (26-32)
  • getSitemap (34-41)
apps/website/src/app/metadata.ts (1)
  • generateBaseMetadata (5-52)
apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card-list.tsx (1)
apps/website/src/hooks/hash-fragment.ts (1)
  • useHashFragment (19-37)
apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/page.tsx (4)
apps/website/src/app/[locale]/(playgrounds)/playgrounds/page.tsx (1)
  • generateMetadata (8-11)
apps/website/src/types.ts (1)
  • PageProps (44-47)
packages/runtimes/src/fetch.ts (1)
  • fetchRuntimeById (60-63)
apps/website/src/app/[locale]/metadata.ts (1)
  • generateBaseMetadata (6-54)
🔇 Additional comments (19)
apps/website/src/components/explorer/use.tsx (1)

12-12: LGTM! Proper i18n integration with Sayable.

The changes correctly integrate the Sayable i18n framework:

  • Hook imported and initialized at component level
  • Toast message properly wrapped in Sayable template literal
  • Dependencies correctly updated to include say

Also applies to: 38-39, 67-67, 69-69

apps/website/src/components/terminal/index.tsx (2)

138-142: Bold markup inside translation: verify translatable markup support.

Embedding inside can break extraction and translator tooling if markup interpolation isn’t supported.

  • If Say supports component placeholders, switch to placeholder-driven emphasis.
  • Otherwise, avoid inline tags inside translated strings (or split text, acknowledging word-order trade-offs).

12-12: Say provider integration verified—no action required.

All concerns from the original review have been confirmed as properly set up:

apps/website/src/components/editor/execute-bar/index.tsx (3)

17-18: Nice integration of Sayable.

Importing { Say, useSay } and initialising say via useSay() looks good and consistent with the rest of the app.

Also applies to: 28-28


118-118: LGTM: placeholder is now localised.

Using saySelect an entry file... for SelectValue is correct.


133-134: LGTM: empty-state string is localised.

Wrapping “No files found” in is appropriate.

apps/website/src/i18n.ts (1)

4-8: LGTM with a small check.

The dynamic JSON import with import(..., { with: { type: 'json' } }) aligns with Next 15. Just make sure the catalogue actually emits JSON as configured in sayable.config.ts.

See the verification script attached to sayable.config.ts.

apps/website/src/components/explorer/index.tsx (4)

24-26: LGTM: say initialisation.

useSay() usage is correct; titles using say templates read well.


42-44: LGTM: heading is localised.

Using for the section title is appropriate.


49-59: LGTM: controls and sr-only labels are localised.

Good use of title={say...} and for screen-reader text.

Also applies to: 62-72, 75-85


108-111: LGTM: empty-state helper text is localised.

Clear and accessible.

apps/website/src/app/[locale]/(playgrounds)/playgrounds/playground-card-list.tsx (1)

63-67: LGTM: stable sort, pinned guard, and localised UI.

  • Copy-before-sort avoids mutating source.
  • Type guard on pinned runtimes is tidy.
  • Toast and select UI strings are correctly localised.

Also applies to: 72-74, 78-83, 113-128

apps/website/src/app/[locale]/layout.tsx (1)

17-24: Params awaited correctly for Next 15.

Awaiting p.params and loading/activating the locale is the right pattern in Next 15. Nice fallback to notFound() on failure. Based on learnings.

apps/website/src/app/[locale]/(editor)/playgrounds/[playground]/page.tsx (6)

3-3: LGTM: correct metadata import.


10-10: LGTM: say import and activation are set up correctly.


29-31: Correct: keep the i18n builder param named say.

This matches your transformer’s expectations. Nice.

Based on learnings


14-17: Static params for child segment look good.

Returning only { playground } is fine since it’s independent of locale and the parent handles [locale].


40-40: LGTM: params usage.

Awaiting props.params and extracting playground matches your PageProps type.


23-24: Let me retry the web search with a refined query:

notFound() is officially supported inside generateMetadata, but verify your Next 15.x version for status code handling.

The redirect() and notFound() Next.js methods can also be used inside generateMetadata. However, be aware that calling notFound() from generateMetadata() may return a 200 status code instead of 404 in Next.js v15.2.0 and later versions, which is a recent regression. Since your code snippet shows notFound() called in the page component itself (not in generateMetadata), your current pattern is safer. If you later add generateMetadata to this file, ensure it returns metadata gracefully for missing resources rather than calling notFound() to avoid status code issues.

@k0d13 k0d13 merged commit e828170 into main Oct 18, 2025
4 checks passed
@k0d13 k0d13 deleted the translate/website branch October 18, 2025 00:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants