Skip to content

Conversation

@fuwenbin
Copy link

@fuwenbin fuwenbin commented Oct 10, 2025

High-level PR Summary

This PR implements internationalization (i18n) for the Stack Auth dashboard, adding support for English and Chinese (Simplified) languages. It introduces next-intl for translation management, creates comprehensive translation files with over 1000+ strings, and adds a language switcher component in the navigation bar. Additionally, it includes developer experience improvements with Docker container management scripts (minimal and full configurations), development server utilities, and documentation for local setup. The i18n implementation covers all major dashboard sections including user management, teams, permissions, emails, payments, and settings.

⏱️ Estimated Review Time: 30-90 minutes

💡 Review Order Suggestion
Order File Path
1 apps/dashboard/src/i18n/routing.ts
2 apps/dashboard/src/i18n/request.ts
3 apps/dashboard/messages/en.json
4 apps/dashboard/messages/zh.json
5 apps/dashboard/next.config.mjs
6 apps/dashboard/package.json
7 apps/dashboard/src/app/layout.tsx
8 apps/dashboard/src/components/language-switcher.tsx
9 apps/dashboard/src/components/navbar.tsx
10 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
11 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
12 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx
13 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx
14 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
15 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx
16 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
17 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/page-client.tsx
18 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/api-keys/page-client.tsx
19 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx
20 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx
21 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
22 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/page-client.tsx
23 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx
24 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx
25 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/transactions/page-client.tsx
26 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-permissions/page-client.tsx
27 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page-client.tsx
28 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx
29 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
30 apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx
31 apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
32 apps/dashboard/src/components/user-dialog.tsx
33 apps/dashboard/src/components/data-table/user-table.tsx
34 apps/dashboard/src/components/data-table/team-table.tsx
35 apps/dashboard/src/components/data-table/team-member-table.tsx
36 apps/dashboard/src/components/data-table/api-key-table.tsx
37 apps/dashboard/src/components/data-table/permission-table.tsx
38 apps/dashboard/src/components/data-table/transaction-table.tsx
39 apps/dashboard/src/components/data-table/payment-product-table.tsx
40 apps/dashboard/src/components/data-table/payment-item-table.tsx
41 packages/stack-ui/src/components/data-table/data-table.tsx
42 packages/stack-ui/src/components/data-table/pagination.tsx
43 packages/stack-ui/src/components/data-table/toolbar.tsx
44 packages/stack-ui/src/components/data-table/view-options.tsx
45 apps/dashboard/src/app/(main)/i18n-test/page.tsx
46 docker/dependencies/README.md
47 docker/dependencies/docker.compose.minimal.yaml
48 scripts/restart-dev-basic.sh
49 scripts/kill-dev-servers.sh
50 scripts/check-docker.sh
51 scripts/fix-docker.sh
52 scripts/start-dev.sh
53 package.json
54 pnpm-lock.yaml
⚠️ Inconsistent Changes Detected
File Path Warning
docker/dependencies/README.md Docker configuration documentation appears unrelated to the i18n feature - this seems to be a separate infrastructure improvement bundled into the PR
docker/dependencies/docker.compose.minimal.yaml New minimal Docker compose configuration is unrelated to internationalization - should be in a separate infrastructure/DevEx PR
scripts/restart-dev-basic.sh Development server management script is unrelated to i18n functionality
scripts/kill-dev-servers.sh Development server cleanup script is unrelated to i18n functionality
scripts/check-docker.sh Docker verification script is unrelated to i18n functionality
scripts/fix-docker.sh Docker troubleshooting script is unrelated to i18n functionality
scripts/start-dev.sh Development environment startup script is unrelated to i18n functionality
package.json New npm scripts for Docker and development management are unrelated to i18n - only the next-intl dependency addition is relevant

Need help? Join our Discord

Analyze latest changes


Important

Adds internationalization support to the Stack Auth dashboard with English and Chinese languages, and includes Docker management scripts and development server utilities.

  • i18n Implementation:
    • Adds next-intl for managing translations in next.config.mjs.
    • Supports English and Chinese (Simplified) languages.
    • Implements language switcher in language-switcher.tsx and integrates it into navbar.tsx and sidebar-layout.tsx.
    • Updates various components and pages to use useTranslations for text, e.g., project-settings/page-client.tsx, users/page-client.tsx, i18n-test/page.tsx.
  • Translation Files:
    • Adds en.json and zh.json with over 1000+ strings for translation.
  • Routing and Request Handling:
    • Configures routing in routing.ts and request handling in request.ts for locale management.
  • Miscellaneous:
    • Adds Docker management scripts (docker.compose.minimal.yaml, check-docker.sh, fix-docker.sh) and development server utilities (kill-dev-servers.sh, restart-dev-basic.sh, start-dev.sh).
    • Updates package.json with new npm scripts for Docker and development management.

This description was created by Ellipsis for aa2321b. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features

    • Internationalization added across the Dashboard with English and Chinese support.
    • Language Switcher in the navbar for quick locale changes.
    • Localized UI across pages, dialogs, navigation, data tables, tooltips, and pagination.
    • Dynamic, translation-driven navigation and breadcrumbs.
    • Added an i18n test page to preview translations.
  • Documentation

    • New guide for Docker dependency environments (full vs. minimal), usage, and FAQs.
  • Chores

    • Minimal Docker compose for dependencies.
    • New scripts to start/stop/restart dev environments, kill servers, check/fix Docker, and health-check services.

…ions

- Integrate next-intl for internationalization support
- Add Chinese (zh) and English (en) translation files
- Add language switcher component in navbar
- Internationalize all data tables (users, teams, transactions, api-keys, permissions, payments)
- Add i18n props to DataTable components in stack-ui
- Translate all page components and dialogs
- Add i18n test page for development
- Add kill-dev-servers.sh to gracefully stop dev processes without affecting Docker
- Add restart-dev-basic.sh for intelligent dev server restart
- Add fix-docker.sh to diagnose and fix Docker daemon issues
- Add check-docker.sh for Docker health verification
- Update start-dev.sh with improved Docker dependency management
- Add docker.compose.minimal.yaml for minimal dependency setup
- Add comprehensive README.md for Docker dependencies
- Add npm scripts: restart-dev-basic and fix-docker
- Improve process tree killing to preserve Docker containers
- Add intelligent port management to avoid Docker conflicts
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 10, 2025

Walkthrough

Adds full internationalization to the dashboard using next-intl: introduces locale routing/config, message bundles (en, zh), provider setup in RootLayout, a LanguageSwitcher, and widespread replacement of hard-coded strings with translations across pages, tables, dialogs, and navigation. Extends stack-ui data table components to accept i18n labels. Adds minimal Docker environment and helper scripts.

Changes

Cohort / File(s) Summary
i18n core integration
apps/dashboard/next.config.mjs, apps/dashboard/package.json, apps/dashboard/src/app/layout.tsx, apps/dashboard/src/i18n/request.ts, apps/dashboard/src/i18n/routing.ts, apps/dashboard/src/app/(main)/i18n-test/page.tsx
Integrates next-intl plugin and provider, loads locale/messages on server, defines routing/locales, and adds an i18n test page. Adds next-intl dependency.
Locale message bundles
apps/dashboard/messages/en.json, apps/dashboard/messages/zh.json
Adds comprehensive English and Chinese translations for dashboard UI, including placeholders and nested namespaces.
Navigation and language switching
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx, apps/dashboard/src/components/language-switcher.tsx, apps/dashboard/src/components/navbar.tsx
Replaces hard-coded navigation with translation-driven items, adds LanguageSwitcher (cookie + refresh), and wires it into the navbar with minor layout tweaks.
Protected pages i18n updates
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx, .../(outside-dashboard)/projects/page-client.tsx, .../projects/[projectId]/api-keys/page-client.tsx, .../domains/page-client.tsx, .../email-drafts/page-client.tsx, .../email-templates/page-client.tsx, .../email-themes/page-client.tsx, .../emails/page-client.tsx, .../payments/transactions/page-client.tsx, .../project-permissions/page-client.tsx, .../project-settings/page-client.tsx, .../team-permissions/page-client.tsx, .../team-settings/page-client.tsx, .../teams/page-client.tsx, .../users/page-client.tsx, .../users/[userId]/page-client.tsx, .../webhooks/page-client.tsx
Replaces UI literals with translation keys across titles, labels, placeholders, tooltips, dialogs, errors/success messages. No business logic changes.
Overview components i18n
.../[projectId]/(overview)/globe.tsx, .../(overview)/line-chart.tsx, .../(overview)/metrics-page.tsx, .../(overview)/setup-page.tsx
Localizes overview charts, globe labels, metrics, and setup workflow strings.
Dashboard data tables i18n
apps/dashboard/src/components/data-table/api-key-table.tsx, payment-item-table.tsx, payment-product-table.tsx, permission-table.tsx, team-member-table.tsx, team-table.tsx, transaction-table.tsx, user-table.tsx
Adds translation hooks, localized column headers, toolbars, pagination labels, actions, and dialogs. Some helpers now accept translation functions; i18n props passed to DataTable.
Form/dialog i18n
apps/dashboard/src/components/user-dialog.tsx
Localizes field labels, hints, validation messages, and dialog text via next-intl.
stack-ui data table i18n API
packages/stack-ui/src/components/data-table/data-table.tsx, pagination.tsx, toolbar.tsx, view-options.tsx
Adds DataTableI18n type and i18n props to table, toolbar, pagination, and view options. New label props and defaults; no behavior changes.
Docker minimal deps and docs
docker/dependencies/docker.compose.minimal.yaml, docker/dependencies/README.md
Adds a minimal Docker dependency stack and documentation.
Dev scripts and npm scripts
scripts/check-docker.sh, scripts/fix-docker.sh, scripts/kill-dev-servers.sh, scripts/restart-dev-basic.sh, scripts/start-dev.sh, package.json
Adds scripts to verify/fix Docker, start/stop minimal deps, restart dev basics, and new npm script entries.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Browser
  participant NextApp as Next.js App (Server)
  participant i18nReq as i18n/request.ts
  participant IntlProv as NextIntlClientProvider

  Browser->>NextApp: Request page
  NextApp->>i18nReq: getLocale() / getMessages()
  i18nReq-->>NextApp: { locale, messages }
  NextApp->>IntlProv: Render with { locale, messages }
  IntlProv-->>Browser: HTML with localized UI
Loading
sequenceDiagram
  autonumber
  participant User
  participant LangSwitch as LanguageSwitcher (Client)
  participant Browser
  participant NextApp as Next.js App (Server)
  participant i18nReq as i18n/request.ts

  User->>LangSwitch: Select language (e.g., zh)
  LangSwitch->>Browser: Set cookie NEXT_LOCALE=zh (1y)
  LangSwitch->>Browser: router.refresh()
  Browser->>NextApp: New request
  NextApp->>i18nReq: Resolve locale (cookie → routing)
  i18nReq-->>NextApp: { locale: zh, messages: zh.json }
  NextApp-->>Browser: Localized response (zh)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • N2D4

Poem

A rabbit taps keys with a multilingual grin,
Flags on the breeze, translations begin.
Tables now whisper in tongues two and bright,
Switch with a click, and locales take flight.
Docker hums softly, seven gears spin—
Hop, hop, hooray! Let the global dash begin. 🐇🌍

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.94% 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
Title Check ✅ Passed The pull request title “dashboard i18n” succinctly identifies the main change of adding internationalization support to the dashboard and is directly related to the primary objective of the PR. It focuses on the core feature without extraneous details and is concise enough for a teammate scanning the history to understand the main update. The phrasing avoids vague terms and clearly conveys the intended scope of the changes.
Description Check ✅ Passed The pull request description begins with the required contributor guidelines comment and includes a thorough high-level summary, detailed changes, review order suggestions, and flagged inconsistencies, satisfying the repository’s minimal template requirement. It provides clear context on the internationalization implementation, Docker and development script updates, and reviewer guidance, ensuring all critical information is present for an effective review. The inclusion of CLA assistant information and nested details offers transparency and completeness. Overall, the description is well-structured and aligned with expectations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@fuwenbin fuwenbin marked this pull request as ready for review October 10, 2025 07:56
"stop-deps:minimal": "POSTGRES_DELAY_MS=0 pnpm run deps-compose:minimal kill && POSTGRES_DELAY_MS=0 pnpm run deps-compose:minimal down -v",
"wait-until-postgres-is-ready:pg_isready": "until pg_isready -h localhost -p 5432; do sleep 1; done",
"wait-until-postgres-is-ready": "command -v pg_isready >/dev/null 2>&1 && pnpm run wait-until-postgres-is-ready:pg_isready || sleep 10 # not everyone has pg_isready installed, so we fallback to sleeping",
"start-deps:no-delay": "pnpm pre && pnpm run deps-compose up --detach --build && pnpm run wait-until-postgres-is-ready && pnpm run db:init && echo \"\\nDependencies started in the background as Docker containers. 'pnpm run stop-deps' to stop them\"n",
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo detected: The echo command in this script ends with "n which appears to be a typo. Possibly it should be \n inside the quotes, or the trailing n should be removed.

Suggested change
"start-deps:no-delay": "pnpm pre && pnpm run deps-compose up --detach --build && pnpm run wait-until-postgres-is-ready && pnpm run db:init && echo \"\\nDependencies started in the background as Docker containers. 'pnpm run stop-deps' to stop them\"n",
"start-deps:no-delay": "pnpm pre && pnpm run deps-compose up --detach --build && pnpm run wait-until-postgres-is-ready && pnpm run db:init && echo \"\\nDependencies started in the background as Docker containers. 'pnpm run stop-deps' to stop them\"",

Copy link

@recurseml recurseml bot left a comment

Choose a reason for hiding this comment

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

Review by RecurseML

🔍 Review performed on 017b43f..aa2321b

✨ No bugs found, your code is sparkling clean

✅ Files analyzed, no issues (50)

apps/dashboard/messages/en.json
apps/dashboard/messages/zh.json
apps/dashboard/next.config.mjs
apps/dashboard/package.json
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/api-keys/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/transactions/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-permissions/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx
apps/dashboard/src/app/(main)/i18n-test/page.tsx
apps/dashboard/src/app/layout.tsx
apps/dashboard/src/components/data-table/api-key-table.tsx
apps/dashboard/src/components/data-table/payment-item-table.tsx
apps/dashboard/src/components/data-table/payment-product-table.tsx
apps/dashboard/src/components/data-table/permission-table.tsx
apps/dashboard/src/components/data-table/team-member-table.tsx
apps/dashboard/src/components/data-table/team-table.tsx
apps/dashboard/src/components/data-table/transaction-table.tsx
apps/dashboard/src/components/data-table/user-table.tsx
apps/dashboard/src/components/language-switcher.tsx
apps/dashboard/src/components/navbar.tsx
apps/dashboard/src/components/user-dialog.tsx
apps/dashboard/src/i18n/request.ts
apps/dashboard/src/i18n/routing.ts
docker/dependencies/README.md
docker/dependencies/docker.compose.minimal.yaml
package.json
packages/stack-ui/src/components/data-table/data-table.tsx
packages/stack-ui/src/components/data-table/pagination.tsx
packages/stack-ui/src/components/data-table/toolbar.tsx
packages/stack-ui/src/components/data-table/view-options.tsx
pnpm-lock.yaml
scripts/check-docker.sh

⏭️ Files skipped (4)
  Locations  
scripts/fix-docker.sh
scripts/kill-dev-servers.sh
scripts/restart-dev-basic.sh
scripts/start-dev.sh

Copy link

@vercel vercel bot left a comment

Choose a reason for hiding this comment

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

Additional Comments:

apps/dashboard/src/middleware.tsx (line 37):
The middleware doesn't integrate with next-intl for locale detection, causing the i18n request handler to always receive undefined for requestLocale and rely entirely on cookies.

View Details
📝 Patch Details
diff --git a/apps/dashboard/src/middleware.tsx b/apps/dashboard/src/middleware.tsx
index 8e71b71e..384b974d 100644
--- a/apps/dashboard/src/middleware.tsx
+++ b/apps/dashboard/src/middleware.tsx
@@ -5,6 +5,8 @@ import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors'
 import { wait } from '@stackframe/stack-shared/dist/utils/promises';
 import type { NextRequest } from 'next/server';
 import { NextResponse } from 'next/server';
+import createIntlMiddleware from 'next-intl/middleware';
+import { routing } from './i18n/routing';
 
 const corsAllowedRequestHeaders = [
   // General
@@ -34,6 +36,8 @@ const corsAllowedResponseHeaders = [
   'x-stack-known-error',
 ];
 
+const intlMiddleware = createIntlMiddleware(routing);
+
 export async function middleware(request: NextRequest) {
   const delay = Number.parseInt(getEnvVariable('STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS', '0'));
   if (delay) {
@@ -48,28 +52,32 @@ export async function middleware(request: NextRequest) {
   const url = new URL(request.url);
   const isApiRequest = url.pathname.startsWith('/api/');
 
-  // default headers
-  const responseInit: ResponseInit = {
-    headers: {
-      // CORS headers
-      ...!isApiRequest ? {} : {
+  // Skip i18n middleware for API routes
+  if (isApiRequest) {
+    // default headers for API routes
+    const responseInit: ResponseInit = {
+      headers: {
+        // CORS headers
         "Access-Control-Allow-Origin": "*",
         "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
         "Access-Control-Allow-Headers": corsAllowedRequestHeaders.join(', '),
         "Access-Control-Expose-Headers": corsAllowedResponseHeaders.join(', '),
       },
-    },
-  };
+    };
+
+    // we want to allow preflight requests to pass through
+    // even if the API route does not implement OPTIONS
+    if (request.method === 'OPTIONS') {
+      return new Response(null, responseInit);
+    }
 
-  // we want to allow preflight requests to pass through
-  // even if the API route does not implement OPTIONS
-  if (request.method === 'OPTIONS' && isApiRequest) {
-    return new Response(null, responseInit);
+    return NextResponse.next(responseInit);
   }
 
-  return NextResponse.next(responseInit);
+  // Handle i18n for non-API routes
+  return intlMiddleware(request);
 }
 
 export const config = {
-  matcher: '/:path*',
+  matcher: ['/((?!api|_next|_vercel|.*\\..*).*)', '/api/v1/auth/signin'],
 };

Analysis

Missing next-intl middleware integration prevents locale detection

What fails: The middleware in apps/dashboard/src/middleware.tsx lacks next-intl integration, causing requestLocale in getRequestConfig() to always be undefined, forcing the system to rely solely on cookies for locale detection.

How to reproduce:

  1. Visit dashboard without existing locale cookie
  2. Check network requests - no x-next-intl-locale header is attached
  3. Server-side rendering defaults to fallback locale instead of browser preference

Result: The locale detection priority order (request → cookie → default) is broken. Browser language preferences are ignored on initial page load.

Expected: Per next-intl middleware docs, middleware should handle locale negotiation and attach locale headers even when localePrefix: 'never' is configured.

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: 11

Caution

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

⚠️ Outside diff range comments (10)
apps/dashboard/src/components/data-table/payment-product-table.tsx (1)

117-126: Localize the delete dialog and toast before shipping

Delete confirmation still renders hard-coded English strings (“Delete Product”, description, “Product deleted” toast). With the new locale switcher, these won’t change when users pick Chinese, so the dialog regresses i18n coverage in this flow. Please source these strings from the tDialog (and, if needed, add a namespace entry for the toast) so the dialog and success feedback honor the active locale.

-        title="Delete Product"
-        description="This action will permanently delete this product."
+        title={tDialog('title')}
+        description={tDialog('description')}-            toast({ title: "Product deleted" });
+            toast({ title: tDialog('successToast') });
apps/dashboard/src/components/data-table/payment-item-table.tsx (1)

129-142: Replace blocking error toasts with inline alerts

These flows return "prevent-close" but surface failures only via toast({ variant: "destructive" }), which violates the dashboard rule, “For blocking alerts and errors in UI, do not use toast notifications; use alerts instead.” Please present the error inside the open dialog (e.g., local alert state, SmartFormDialog error slot) so the message stays anchored to the blocking action. As per coding guidelines.

Also applies to: 181-192

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (2)

445-451: Translate Yup validation error message.

The form schema contains a hardcoded English validation error message. This should use a translation key for consistency.

Apply this diff:

+  const tValidation = useTranslations('userDetail.contactChannels.sendEmailDialog.validation');
   formSchema={yup.object({
     selected: yup.string().defined(),
-    localhostPort: yup.number().test("required-if-localhost", "Required if localhost is selected", (value, context) => {
+    localhostPort: yup.number().test("required-if-localhost", tValidation('localhostPortRequired'), (value, context) => {
       return context.parent.selected === "localhost" ? value !== undefined : true;
     }),

836-885: Translate Yup validation error messages.

The form schema contains multiple hardcoded English validation error messages (lines 838-839, 857-858, 864). These should use translation keys for consistency with the i18n implementation.

Apply this diff:

+  const tValidation = useTranslations('userDetail.oauthProviders.dialog.validation');
   const formSchema = yup.object({
     providerId: yup.string()
-      .defined("Provider is required")
-      .nonEmpty("Provider is required")
+      .defined(tValidation('providerRequired'))
+      .nonEmpty(tValidation('providerRequired'))
       .label(tFields('provider'))
       .meta({
         // ...
       }),
     email: yup.string()
-      .email("Please enter a valid e-mail address")
+      .email(tValidation('invalidEmail'))
       .optional()
       .label(tFields('email'))
       .meta({
         // ...
       }),
     accountId: yup.string()
-      .defined("Account ID is required")
+      .defined(tValidation('accountIdRequired'))
       .label(tFields('accountId'))
apps/dashboard/src/components/data-table/transaction-table.tsx (1)

65-65: Untranslated column header: Product / Item.

Line 65 has a hardcoded English header while all other columns use translations. This creates an inconsistent localization experience.

Apply this diff to add translation:

   {
     accessorKey: 'product_or_item',
-    header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Product / Item" />,
+    header: ({ column }) => <DataTableColumnHeader column={column} columnTitle={t('productOrItem')} />,
     cell: ({ row }) => (

And ensure the corresponding translation key exists in the message files:

{
  "transactions": {
    "table": {
      "columns": {
        "productOrItem": "Product / Item"
      }
    }
  }
}
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx (1)

47-80: Incomplete i18n coverage in dialog and form elements.

The page title and description are properly localized, but several user-facing strings remain hardcoded within the dialog:

  • Line 53: "Currently using ${selectedThemeData.displayName}" (partially hardcoded)
  • Line 59: "Set Theme"
  • Line 62: "Select Email Theme"
  • Line 65: "Save Theme"
  • Lines 119-146: NewThemeButton dialog strings ("New Theme", "Theme Name", "Enter theme name")

To complete the i18n implementation, add these strings to the emailThemes namespace in en.json/zh.json and replace them with translation calls. For example:

       <SettingCard
         title={t('activeTheme.title')}
-        description={`Currently using ${selectedThemeData.displayName}`}
+        description={t('activeTheme.description', { themeName: selectedThemeData.displayName })}
       >

Then add to emailThemes in translation files:

{
  "activeTheme": {
    "title": "Active Theme",
    "description": "Currently using {themeName}"
  }
}
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/page-client.tsx (1)

83-111: Localize the NewTemplateButton component.

The NewTemplateButton function uses hard-coded strings ("New Template", "Template Name", "Enter template name"), which is inconsistent with the i18n integration applied to the rest of the file.

Apply this diff to localize the component:

 function NewTemplateButton() {
+  const t = useTranslations('emailTemplates');
   const stackAdminApp = useAdminApp();
   const router = useRouter();

   const handleCreateNewTemplate = async (values: { name: string }) => {
     const { id } = await stackAdminApp.createEmailTemplate(values.name);
     router.push(`email-templates/${id}`);
   };

   return (
     <FormDialog
-      title="New Template"
-      trigger={<Button>New Template</Button>}
+      title={t('newTemplate.title')}
+      trigger={<Button>{t('newTemplate.button')}</Button>}
       onSubmit={handleCreateNewTemplate}
       formSchema={yup.object({
         name: yup.string().defined(),
       })}
       render={(form) => (
         <InputField
           control={form.control}
           name="name"
-          label="Template Name"
-          placeholder="Enter template name"
+          label={t('newTemplate.nameLabel')}
+          placeholder={t('newTemplate.namePlaceholder')}
           required
         />
       )}
     />
   );
 }

Then add the corresponding keys to apps/dashboard/messages/en.json:

"emailTemplates": {
  ...
  "newTemplate": {
    "title": "New Template",
    "button": "New Template",
    "nameLabel": "Template Name",
    "namePlaceholder": "Enter template name"
  }
}
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx (2)

287-293: Bug: config shape inconsistency (likely runtime error)

You read project.config.emailConfig, but elsewhere use project.useConfig().emails.server. This mismatch will break checks and sendTestEmail config.

 function TestSendingDialog(props: {
   trigger: React.ReactNode,
 }) {
   const stackAdminApp = useAdminApp();
   const project = stackAdminApp.useProject();
   const { toast } = useToast();
   const [error, setError] = useState<string | null>(null);
+  const config = project.useConfig();
...
   onSubmit={async (values) => {
-      const emailConfig = project.config.emailConfig || throwErr("Email config is not set");
-      if (emailConfig.type === 'shared') throwErr("Shared email server cannot be used for testing");
+      const emailConfig = config.emails.server || throwErr("Email config is not set");
+      if (emailConfig.isShared) throwErr("Shared email server cannot be used for testing");
 
       const result = await stackAdminApp.sendTestEmail({
         recipientEmail: values.email,
-        emailConfig: emailConfig,
+        emailConfig,
       });

427-434: Blocking error uses toast; use inline Alert instead

This prevents close and informs the user; per guidelines, avoid toast for blocking errors. Show an inline Alert in the dialog.

As per coding guidelines

-    if (selectedUsers.length === 0) {
-      toast({
-        title: "No recipients selected",
-        description: "Please select at least one recipient to send the email.",
-        variant: "destructive",
-      });
-      return "prevent-close" as const;
-    }
+    if (selectedUsers.length === 0) {
+      setRecipientsError(tSend('selectRecipientsError')); // e.g., "Please select at least one recipient."
+      return "prevent-close" as const;
+    }

And add local state and inline Alert:

 function SendEmailDialog(props: {
   trigger: React.ReactNode,
   emailConfig: CompleteConfig['emails']['server'],
 }) {
   const stackAdminApp = useAdminApp();
   const { toast } = useToast();
   const [open, setOpen] = useState(false);
   const [sharedSmtpDialogOpen, setSharedSmtpDialogOpen] = useState(false);
   const [selectedUsers, setSelectedUsers] = useState<ServerUser[]>([]);
   const [stage, setStage] = useState<'recipients' | 'data'>('recipients');
+  const [recipientsError, setRecipientsError] = useState<string | null>(null);
+  const tSend = useTranslations('emails.sendDialog');
...
             {stage === 'recipients' ? (
               <TeamMemberSearchTable
                 action={(user) => (
                   <Button
                     size="sm"
                     variant="outline"
                     onClick={() => setSelectedUsers(users =>
                       users.some(u => u.id === user.id)
                         ? users.filter(u => u.id !== user.id)
                         : [...users, user]
                     )}
                   >
                     {selectedUsers.some(u => u.id === user.id) ? 'Remove' : 'Add'}
                   </Button>
                 )}
               />
             ) : (

And render an Alert when error:

-            {stage === 'recipients' ? (
+            {stage === 'recipients' ? (
+              <>
+                {recipientsError && <Alert variant="destructive">{recipientsError}</Alert>}
               <TeamMemberSearchTable
apps/dashboard/src/components/data-table/team-member-table.tsx (1)

75-94: Localize Cancel label in EditPermissionDialog

Ensure Cancel is translated.

   return <SmartFormDialog
     open={props.open}
     onOpenChange={props.onOpenChange}
     title={t('title')}
     formSchema={formSchema}
     okButton={{ label: t('save') }}
@@
-    cancelButton
+    cancelButton={{ label: t('cancel') }}
   />;
🧹 Nitpick comments (28)
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx (1)

29-36: Memoize the schema to avoid recreation on every render.

The projectFormSchema is recreated on every component render, which is inefficient. Since the schema depends on the translation function t, wrap it in useMemo with t as a dependency.

Apply this diff:

+import { useMemo } from "react";

 export default function PageClient() {
   const t = useTranslations('newProject');
   const user = useUser({ or: 'redirect', projectIdMustMatch: "internal" });
   const [loading, setLoading] = useState(false);
   const router = useRouter();
   const searchParams = useSearchParams();
   const displayName = searchParams.get("display_name");
 
-  const projectFormSchema = yup.object({
-    displayName: yup.string().min(1, t('validation.displayNameRequired')).defined().nonEmpty(t('validation.displayNameRequired')),
-    signInMethods: yup.array(yup.string().oneOf(["credential", "magicLink", "passkey"].concat(allProviders)).defined())
-      .min(1, t('validation.signInMethodRequired'))
-      .defined(t('validation.signInMethodRequired')),
-    teamId: yup.string().uuid().defined(t('validation.teamRequired')),
-  });
+  const projectFormSchema = useMemo(() => yup.object({
+    displayName: yup.string().min(1, t('validation.displayNameRequired')).defined().nonEmpty(t('validation.displayNameRequired')),
+    signInMethods: yup.array(yup.string().oneOf(["credential", "magicLink", "passkey"].concat(allProviders)).defined())
+      .min(1, t('validation.signInMethodRequired'))
+      .defined(t('validation.signInMethodRequired')),
+    teamId: yup.string().uuid().defined(t('validation.teamRequired')),
+  }), [t]);
apps/dashboard/src/components/user-dialog.tsx (1)

67-73: Consider using tErrors namespace for validation messages.

Line 69 uses tHints('emailVerifiedRequired') for a Yup validation error message, while the same key is reused as a UI hint on line 151. Validation error messages typically belong in the error namespace for consistency and clarity. Consider either:

  • Using tErrors('emailVerifiedRequired') here and keeping tHints('emailVerifiedRequired') for the UI hint (separate keys with the same text in translation files), or
  • Moving this to tErrors and referencing that namespace on line 151 if the same message is appropriate in both contexts.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (2)

254-257: Avoid shadowing the translation function t.

The callback parameter in teams.find(t => ...) on line 256 shadows the translation function t. While this works due to scoping, it reduces code clarity.

Apply this diff to use a clearer parameter name:

 {t('transfer.confirm', {
   projectName: project.displayName,
-  teamName: teams.find(t => t.id === selectedTeamId)?.displayName
+  teamName: teams.find(team => team.id === selectedTeamId)?.displayName
 })}

304-304: Consider consolidating sentence fragments for better translation flexibility.

Splitting a sentence across multiple translation keys (line 304) can cause issues in languages with different word orders. The bold text might need to appear in a different position in some languages.

Consider using a single translation key with ICU rich text formatting (next-intl supports this in v4+):

// In your translation file:
{
  "dangerZone": {
    "irreversibleWarning": "This action is <bold>irreversible</bold>. This will permanently delete:"
  }
}

// In the component:
{t.rich('dangerZone.irreversibleWarning', {
  bold: (chunks) => <strong>{chunks}</strong>
})}

Alternatively, if keeping the current approach, ensure translators understand the context and sentence structure in all languages.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx (1)

29-29: Consider using a single translation key with rich text formatting.

The welcome message currently uses multiple separate translation calls concatenated together. This approach can make translation difficult as translators may not have full context.

Consider refactoring to use a single translation key with ICU message format for rich text:

{t.rich('welcomeMessage', {
  link: (chunks) => <StyledLink href="https://docs.stack-auth.com">{chunks}</StyledLink>
})}

And update the translation file to:

{
  "welcomeMessage": "Welcome to Stack Auth! Check out our <link>documentation</link> to get started."
}

This approach provides translators with full context and allows them to reorder elements as needed for different languages.

docker/dependencies/docker.compose.minimal.yaml (1)

108-108: Minor formatting: Remove extra blank line.

YAMLlint reports too many blank lines at the end of the file.

 volumes:
   postgres-data:
   inbucket-data:
   svix-redis-data:
   svix-postgres-data:
   s3mock-data:
   deno-cache:
-
apps/dashboard/messages/zh.json (1)

1-1053: LGTM! Translation file structure is well-organized.

The Chinese translation file is comprehensive and properly structured to match the dashboard's i18n namespace hierarchy. A spot check reveals consistent placeholder formatting (e.g., {userName}, {teamName}, {page}) and no obvious HTML injection risks.

However, consider these optional improvements:

  1. Translation quality verification: Have a native Chinese speaker review technical terminology and UI strings for naturalness and accuracy.
  2. Placeholder validation: Consider adding a build-time check to ensure all placeholders in translations match those in the English source (en.json).
apps/dashboard/src/components/language-switcher.tsx (2)

27-27: Consider smoother locale transition without full page refresh.

Calling router.refresh() triggers a full page reload, which may cause a jarring user experience. Next-intl and Next.js App Router support smoother locale transitions.

Consider using Next.js's useRouter from next/navigation with a locale-aware approach, or investigate if next-intl provides a smoother transition mechanism that updates locale context without a hard refresh. The current implementation works but could be improved for better UX.

If a refresh is necessary due to server-side locale detection, document this decision in a comment explaining why a full refresh is required.


13-16: Hardcoded language list limits extensibility.

The languages array is hardcoded in the component. If more languages are added to the routing configuration (apps/dashboard/src/i18n/routing.ts), this list must be manually updated in sync.

Consider deriving the available languages from the routing configuration or a shared constants file:

// src/i18n/languages.ts
export const languages = [
  { code: 'en', name: 'English', flag: '🇺🇸' },
  { code: 'zh', name: '中文', flag: '🇨🇳' },
] as const;

// Validate against routing config at build time
import { routing } from './routing';
const localeSet = new Set(routing.locales);
languages.forEach(lang => {
  if (!localeSet.has(lang.code)) {
    throw new Error(`Language ${lang.code} not in routing config`);
  }
});

Then import and use this shared constant in both the language switcher and any other components that need the language list.

apps/dashboard/src/i18n/request.ts (2)

16-16: Type assertions bypass type safety for locale validation.

The code uses as any type assertions when checking if the locale is included in routing.locales. This bypasses TypeScript's type checking and could mask type mismatches.

Consider adding proper typing or using a type guard:

// In routing.ts, export locale type
export type Locale = typeof routing.locales[number];

// In request.ts
import { routing, type Locale } from './routing';

function isValidLocale(locale: string): locale is Locale {
  return routing.locales.includes(locale as Locale);
}

// Then use:
if (localeCookie && isValidLocale(localeCookie.value)) {
  locale = localeCookie.value;
}

if (!locale || !isValidLocale(locale)) {
  locale = routing.defaultLocale;
}

This provides better type safety and makes the intent clearer.

Also applies to: 22-22


26-29: Add error handling for missing locale message files.

The dynamic import at line 28 will throw an unhandled error if a locale's message file is missing or malformed. While this should not occur in production, it could cause issues during development or if the build process has problems.

Add error handling and a fallback:

  try {
    return {
      locale,
      messages: (await import(`../../messages/${locale}.json`)).default
    };
  } catch (error) {
    console.error(`Failed to load messages for locale "${locale}":`, error);
    // Fallback to default locale messages
    return {
      locale: routing.defaultLocale,
      messages: (await import(`../../messages/${routing.defaultLocale}.json`)).default
    };
  }

This prevents the entire app from crashing if a locale file is missing and provides a graceful degradation path.

scripts/fix-docker.sh (1)

1-105: Consider using English for script messages.

This script is part of an i18n PR focused on dashboard UI localization, yet the script itself uses Chinese-only messages. For developer tooling and scripts, English is the conventional choice to ensure wider accessibility and maintainability across international teams.

Apply this diff to replace Chinese messages with English:

-echo "🔧 Docker Desktop 修复工具"
+echo "🔧 Docker Desktop Repair Tool"
 echo "================================"
 echo ""

-# Step 1: 检查 Docker Desktop 状态
-echo "📋 Step 1: 检查当前状态..."
+# Step 1: Check Docker Desktop status
+echo "📋 Step 1: Checking current status..."
 if pgrep -q "Docker Desktop"; then
-    echo "   ✅ Docker Desktop UI 正在运行"
+    echo "   ✅ Docker Desktop UI is running"
 else
-    echo "   ❌ Docker Desktop UI 未运行"
+    echo "   ❌ Docker Desktop UI is not running"
 fi

 if pgrep -q "com.docker.backend"; then
-    echo "   ✅ Docker Backend 正在运行"
+    echo "   ✅ Docker Backend is running"
     echo ""
-    echo "Docker 看起来正常,尝试连接..."
-    docker ps > /dev/null 2>&1 && echo "   ✅ Docker 工作正常!" && exit 0
+    echo "Docker appears healthy, attempting connection..."
+    docker ps > /dev/null 2>&1 && echo "   ✅ Docker is working!" && exit 0
 else
-    echo "   ❌ Docker Backend 未运行(这是问题所在!)"
+    echo "   ❌ Docker Backend is not running (this is the problem!)"
 fi

 echo ""

-# Step 2: 完全停止 Docker
-echo "📋 Step 2: 完全停止 Docker Desktop..."
+# Step 2: Stop Docker completely
+echo "📋 Step 2: Stopping Docker Desktop completely..."
 osascript -e 'quit app "Docker"' 2>/dev/null || true
 sleep 3

-# 强制杀死所有 Docker 进程
-echo "   → 清理残留进程..."
+# Force kill all Docker processes
+echo "   → Cleaning up remaining processes..."
 pkill -9 -f "Docker Desktop" 2>/dev/null || true
 pkill -9 -f "com.docker" 2>/dev/null || true
 sleep 2

-# Step 3: 清理可能的锁文件
-echo "📋 Step 3: 清理锁文件和状态..."
+# Step 3: Clean potential lock files
+echo "📋 Step 3: Cleaning lock files and state..."
 rm -f ~/Library/Group\ Containers/group.com.docker/docker.sock.lock 2>/dev/null || true
 rm -f ~/.docker/run/*.lock 2>/dev/null || true
-echo "   ✅ 清理完成"
+echo "   ✅ Cleanup complete"
 echo ""

-# Step 4: 重启 Docker Desktop
-echo "📋 Step 4: 重新启动 Docker Desktop..."
+# Step 4: Restart Docker Desktop
+echo "📋 Step 4: Restarting Docker Desktop..."
 open -a Docker

-echo "   等待 Docker Desktop 启动..."
+echo "   Waiting for Docker Desktop to start..."
 sleep 10

-# 等待最多 60 秒让 Docker 完全启动
-echo "   检查 Docker daemon 是否准备就绪..."
+# Wait up to 60 seconds for Docker to fully start
+echo "   Checking if Docker daemon is ready..."
 COUNTER=0
 MAX_WAIT=60

 while [ $COUNTER -lt $MAX_WAIT ]; do
     if docker info > /dev/null 2>&1; then
-        echo "   ✅ Docker daemon 已就绪!"
+        echo "   ✅ Docker daemon is ready!"
         break
     fi
     
     if pgrep -q "com.docker.backend"; then
-        echo "   ⏳ Backend 进程已启动,等待就绪... ($COUNTER/$MAX_WAIT)"
+        echo "   ⏳ Backend process started, waiting for ready... ($COUNTER/$MAX_WAIT)"
     else
-        echo "   ⏳ 等待 Backend 进程启动... ($COUNTER/$MAX_WAIT)"
+        echo "   ⏳ Waiting for Backend process to start... ($COUNTER/$MAX_WAIT)"
     fi
     
     sleep 2
     COUNTER=$((COUNTER + 2))
 done

 echo ""

-# Step 5: 验证
-echo "📋 Step 5: 验证 Docker 状态..."
+# Step 5: Verify
+echo "📋 Step 5: Verifying Docker status..."
 if docker info > /dev/null 2>&1; then
-    echo "   ✅ Docker 工作正常!"
+    echo "   ✅ Docker is working!"
     echo ""
     docker version
     echo ""
     echo "================================"
-    echo "✨ 修复成功!现在可以启动开发环境了:"
+    echo "✨ Repair successful! You can now start the dev environment:"
     echo "   pnpm run start-deps:minimal"
     echo "================================"
     exit 0
 else
-    echo "   ❌ Docker 仍然无法连接"
+    echo "   ❌ Docker still cannot connect"
     echo ""
     echo "================================"
-    echo "⚠️  自动修复失败,请尝试:"
+    echo "⚠️  Auto-repair failed, please try:"
     echo ""
-    echo "1. 手动重启 Docker Desktop:"
-    echo "   - 点击菜单栏的 Docker 图标"
-    echo "   - 选择 'Restart'"
+    echo "1. Manually restart Docker Desktop:"
+    echo "   - Click the Docker icon in the menu bar"
+    echo "   - Select 'Restart'"
     echo ""
-    echo "2. 如果还不行,完全卸载重装:"
-    echo "   - 在 Docker Desktop 中选择 'Troubleshoot' > 'Clean / Purge data'"
-    echo "   - 或者完全卸载后重新安装"
+    echo "2. If that doesn't work, fully uninstall and reinstall:"
+    echo "   - In Docker Desktop select 'Troubleshoot' > 'Clean / Purge data'"
+    echo "   - Or completely uninstall and reinstall"
     echo ""
-    echo "3. 检查系统日志查看错误:"
+    echo "3. Check system logs for errors:"
     echo "   tail -f ~/Library/Containers/com.docker.docker/Data/log/vm/dockerd.log"
     echo "================================"
     exit 1
 fi
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx (1)

50-50: Simplify the namespace override for consistency.

The error message uses t('globe.errorMessage', { ns: 'overview' }) with a namespace override. Since you're already in the 'overview.metrics' namespace and there's a globe.errorMessage key at overview.metrics.globe.errorMessage in en.json, you can simplify this to t('globe.errorMessage') without the namespace override for better clarity and consistency.

Apply this diff:

-<ErrorBoundary fallback={<div className='text-center text-sm text-red-500'>{t('globe.errorMessage', { ns: 'overview' })}</div>}>
+<ErrorBoundary fallback={<div className='text-center text-sm text-red-500'>{t('globe.errorMessage')}</div>}>
scripts/restart-dev-basic.sh (1)

24-34: Add a comment explaining the container count requirement.

The script checks for at least 7 running containers, but this number might be unclear to future maintainers. Adding a brief comment would improve clarity.

Apply this diff:

+# Minimal stack requires 7 containers: postgres, inbucket, svix, redis, etc.
 RUNNING_CONTAINERS=$(docker ps --filter "name=stack-dependencies" --format '{{.Names}}' | wc -l)

 if [ "$RUNNING_CONTAINERS" -lt 7 ]; then
packages/stack-ui/src/components/data-table/pagination.tsx (1)

13-17: Solid i18n extension with safe defaults; consider minor a11y tweak

Props and defaults look good. Optional: expose previous/next labels via aria-label on the Buttons (in addition to sr-only) for clearer accessible names.

-            <Button
+            <Button
               variant="outline"
               className="h-8 w-8 p-0"
               onClick={() => table.previousPage()}
               disabled={!table.getCanPreviousPage()}
+              aria-label={previousPageLabel}
             >
               <span className="sr-only">{previousPageLabel}</span>
               <ChevronLeftIcon className="h-4 w-4" />
             </Button>
...
-            <Button
+            <Button
               variant="outline"
               className="h-8 w-8 p-0"
               onClick={() => table.nextPage()}
               disabled={!table.getCanNextPage()}
+              aria-label={nextPageLabel}
             >
               <span className="sr-only">{nextPageLabel}</span>
               <ChevronRightIcon className="h-4 w-4" />
             </Button>

Also applies to: 21-25

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx (2)

321-341: Use next-intl formatter for locale-aware date/time

Leverage useFormatter for consistent i18n formatting.
Based on learnings

-import { useTranslations } from 'next-intl';
+import { useTranslations, useFormatter } from 'next-intl';
...
 const useEmailTableColumns = (): ColumnDef<AdminSentEmail>[] => {
   const t = useTranslations('emails.emailLog.columns');
+  const format = useFormatter();
   return [
     { accessorKey: 'recipient', header: t('recipient') },
     { accessorKey: 'subject', header: t('subject') },
     {
-      accessorKey: 'sentAt', header: t('sentAt'), cell: ({ row }) => {
-        const date = row.original.sentAt;
-        return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
-      }
+      accessorKey: 'sentAt',
+      header: t('sentAt'),
+      cell: ({ row }) => format.dateTime(row.original.sentAt, { dateStyle: 'medium', timeStyle: 'short' })
     },

Also applies to: 327-331


169-175: i18n gaps: several hard-coded strings remain

Multiple labels, titles, button texts, and descriptions are still hard-coded; convert to useTranslations for full i18n coverage.

Examples:

  • Dialog titles/buttons: "Edit Email Server", "Save", "Send a Test Email", "Send", "Send Email", "Cancel", "Back", "Next".
  • Field labels/options: "Email server", "Shared (noreply@stackframe.co)", "Resend ...", "Custom SMTP server ...", "Resend API Key", "Sender Email/Name", "Host/Port/Username/Password", "Notification Category", "Transactional/Marketing", "Email Content", "Subject".
  • UI text: "Recipients", "Shared Email Server", "Warning", and the shared-server alert description.
  • TeamMemberSearchTable action buttons: "Add"/"Remove".

If helpful, I can generate a targeted patch applying t('...') across these spots.

Also applies to: 221-229, 231-244, 247-264, 281-286, 314-314, 450-453, 498-510, 512-522, 551-552, 557-567, 569-574

apps/dashboard/src/components/data-table/team-table.tsx (1)

112-132: Type t for safer translations

Avoid any; type t to ReturnType (or a narrowed namespace type).

-const getColumns = (t: any): ColumnDef<ServerTeam>[] =>  [
+const getColumns = (t: ReturnType<typeof useTranslations>): ColumnDef<ServerTeam>[] => [
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx (1)

535-537: Localize Suspense fallback

"LOADING" is hard-coded; use a translated string.

-function GlobeIllustration() {
+function GlobeIllustration() {
+  const t = useTranslations('overview.setup');
   return (
     <div className="w-[200px] h-[200px] relative hidden md:block">
-      <Suspense fallback={"LOADING"}>
+      <Suspense fallback={t('loadingGlobe')}>
         <GlobeIllustrationInner />
       </Suspense>
     </div>
   );
 }
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx (1)

585-587: Optional: i18n for "Sidebar Menu"

Translate SheetTitle to align with the rest of the navigation texts.

-  <SheetTitle className="hidden">
-    Sidebar Menu
-  </SheetTitle>
+  <SheetTitle className="hidden">
+    {t('sidebarMenu')}
+  </SheetTitle>
scripts/kill-dev-servers.sh (4)

1-4: Enable strict mode and pipefail for safer execution

Prevents silent failures and masked pipeline errors; add ERR traps if desired.

 #!/bin/bash
+
+# Fail fast and catch pipeline errors
+set -Eeuo pipefail
+# Optional: basic trap
+trap 'echo "kill-dev-servers aborted"; exit 1' ERR

12-29: Fix SC2155 and quote vars in kill_process_tree

Avoids masking return values and handles PIDs with defensive quoting. Based on static analysis hints.

 kill_process_tree() {
-    local pid=$1
-    local sig=${2:-TERM}
+    local pid
+    pid="$1"
+    local sig
+    sig="${2:-TERM}"
@@
-    local children=$(pgrep -P $pid 2>/dev/null)
+    local children
+    children=$(pgrep -P "$pid" 2>/dev/null || true)
@@
-        kill_process_tree $child $sig
+        kill_process_tree "$child" "$sig"
@@
-    if kill -0 $pid 2>/dev/null; then
-        kill -$sig $pid 2>/dev/null || true
+    if kill -0 "$pid" 2>/dev/null; then
+        kill -"${sig}" "$pid" 2>/dev/null || true
     fi
 }

68-89: Use graceful tree kill before -9 on port PIDs

Blind kill -9 risks leaving children/orphans. Reuse kill_process_tree with TERM then KILL.

 for port in "${DEV_PORTS[@]}"; do
   pids=$(lsof -ti:$port 2>/dev/null)
   if [ ! -z "$pids" ]; then
     echo "    ✓ Killing processes on port $port"
-    echo "$pids" | xargs kill -9 2>/dev/null || true
+    for pid in $pids; do
+      kill_process_tree "$pid" TERM
+      sleep 0.3
+      # Ensure termination if still alive
+      kill_process_tree "$pid" KILL
+    done
   fi
 done
@@
   for port in "${DOCKER_PORTS[@]}"; do
     pids=$(lsof -ti:$port 2>/dev/null)
     if [ ! -z "$pids" ]; then
       echo "    ✓ Killing processes on port $port"
-      echo "$pids" | xargs kill -9 2>/dev/null || true
+      for pid in $pids; do
+        kill_process_tree "$pid" TERM
+        sleep 0.3
+        kill_process_tree "$pid" KILL
+      done
     fi
   done

91-93: Remove unused ALL_PORTS variable

SC2034: variable is never used.

-# Combine all ports for verification
-ALL_PORTS=("${DEV_PORTS[@]}" "${DOCKER_PORTS[@]}")
scripts/start-dev.sh (2)

1-4: Harden script with pipefail and traps

Prevents masked failures (esp. pipelines) and improves resiliency.

 #!/bin/bash
 
-set -e  # Exit on any error
+set -Eeuo pipefail  # Exit on errors, unset vars, and pipeline failures
+# Optional: trap failures for cleanup/log
+trap 'echo "start-dev aborted"; exit 1' ERR

46-47: Pipeline masks errors from kill script

With tail, failures in kill-dev-servers.sh won’t fail this step unless pipefail is set. Either set pipefail (above) or avoid piping.

-bash "$(dirname "$0")/kill-dev-servers.sh" | tail -n 5
+if ! bash "$(dirname "$0")/kill-dev-servers.sh"; then
+  echo "❌ Failed to clean up dev servers"; exit 1
+fi
apps/dashboard/src/components/data-table/api-key-table.tsx (1)

12-22: Consider i18n for DataTableFacetedFilter internal strings

Faceted filter currently renders “No results found.” and “Clear filters” in English (see packages/stack-ui/.../faceted-filter.tsx). Add i18n props there and pass translations here.

packages/stack-ui/src/components/data-table/data-table.tsx (1)

46-135: Consider adding i18n support for the "No results." string.

The i18n prop is correctly propagated to DataTableToolbar and DataTablePagination. However, the hardcoded "No results." string at line 119 is not exposed through the i18n interface.

To fully internationalize the component, add a noResults field to DataTableI18n:

 export type DataTableI18n = {
   resetFilters?: string,
   exportCSV?: string,
   noDataToExport?: string,
   view?: string,
   toggleColumns?: string,
   rowsSelected?: (selected: number, total: number) => string,
   rowsPerPage?: string,
   previousPage?: string,
   nextPage?: string,
+  noResults?: string,
 };

Then update line 119:

-                  No results.
+                  {props.i18n?.noResults || "No results."}

"next-intl": "^4.3.11",
"next-themes": "^0.2.1",
"posthog-js": "^1.235.0",
"react": "19.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

React version mismatch between dependencies and type overrides.

Line 53 specifies react: 19.0.0, while the pnpm overrides at line 86 specify @types/react: 19.0.12. This inconsistency may lead to type mismatches if React 19.0.0 has a different API surface than the types expect.

Run the following script to verify whether React 19.0.0 and 19.0.12 exist and check for breaking changes:

Also applies to: 86-86


🏁 Script executed:

#!/bin/bash
# Check for React 19.0.0 and 19.0.12 releases and their differences

echo "=== Checking React releases ==="
npm view react@19.0.0 version 2>/dev/null || echo "react@19.0.0 not found"
npm view react@19.0.12 version 2>/dev/null || echo "react@19.0.12 not found"

echo ""
echo "=== Checking @types/react releases ==="
npm view @types/react@19.0.12 version 2>/dev/null || echo "@types/react@19.0.12 not found"

Length of output: 110


Align React and @types/react versions.
react@19.0.0 exists but react@19.0.12 does not; update your override so both runtime and types match (e.g. pin @types/react to 19.0.0 or use ^19.0.0) to avoid type mismatches.

🤖 Prompt for AI Agents
In apps/dashboard/package.json around line 53, the project pins "react" to
19.0.0 while the @types/react override/resolution is mismatched (refers to a
non-existent 19.0.12); update the package.json overrides/resolutions so
@types/react matches the runtime version (e.g., pin @types/react to 19.0.0 or
use a compatible range like ^19.0.0), then run install to refresh lockfile so
runtime and types are aligned.

} catch (error) {
console.error('Failed to transfer project:', error);
alert(`Failed to transfer project: ${error instanceof Error ? error.message : 'Unknown error'}`);
alert(t('transfer.error', { message: error instanceof Error ? error.message : 'Unknown error' }));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace browser alert() with a proper UI component.

Using the browser's native alert() provides a poor user experience (blocks the entire page, cannot be styled, inconsistent across browsers).

Consider using the Alert component from stack-ui or displaying the error in the transfer dialog itself:

-      alert(t('transfer.error', { message: error instanceof Error ? error.message : 'Unknown error' }));

Then update the transfer section to display the error state. For example, add error state handling:

const [transferError, setTransferError] = useState<string | null>(null);

// In the catch block:
setTransferError(error instanceof Error ? error.message : 'Unknown error');

// In the JSX, add before the transfer controls:
{transferError && (
  <Alert variant="destructive">
    {t('transfer.error', { message: transferError })}
  </Alert>
)}
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
around line 65, replace the browser alert(...) call with state-driven UI error
handling: add a transferError state (string | null), set transferError in the
catch block to error.message or 'Unknown error' instead of calling alert, import
and render the Alert component (e.g., Alert variant="destructive") above the
transfer controls to show t('transfer.error', { message: transferError }) when
transferError is non-null, and ensure to clear transferError when the dialog
closes or on retry; also add necessary imports for useState and Alert.

Comment on lines +287 to +299
name: (pathname: string) => {
const match = pathname.match(/^\/projects\/[^\/]+\/webhooks\/([^\/]+)$/);
let href;
if (match) {
href = `/teams/${match[1]}`;
} else {
href = "";
}

return [
{ item: t('webhooks'), href: "/webhooks" },
{ item: t('endpoint'), href },
];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Wrong breadcrumb link for webhook endpoint

Link points to /teams/{id}; should be /webhooks/{id}.

-        const match = pathname.match(/^\/projects\/[^\/]+\/webhooks\/([^\/]+)$/);
+        const match = pathname.match(/^\/projects\/[^\/]+\/webhooks\/([^\/]+)$/);
         let href;
         if (match) {
-          href = `/teams/${match[1]}`;
+          href = `/webhooks/${match[1]}`;
         } else {
           href = "";
         }
 
         return [
           { item: t('webhooks'), href: "/webhooks" },
           { item: t('endpoint'), href },
         ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name: (pathname: string) => {
const match = pathname.match(/^\/projects\/[^\/]+\/webhooks\/([^\/]+)$/);
let href;
if (match) {
href = `/teams/${match[1]}`;
} else {
href = "";
}
return [
{ item: t('webhooks'), href: "/webhooks" },
{ item: t('endpoint'), href },
];
name: (pathname: string) => {
const match = pathname.match(/^\/projects\/[^\/]+\/webhooks\/([^\/]+)$/);
let href;
if (match) {
href = `/webhooks/${match[1]}`;
} else {
href = "";
}
return [
{ item: t('webhooks'), href: "/webhooks" },
{ item: t('endpoint'), href },
];
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
around lines 287 to 299, the breadcrumb link resolver extracts an ID from the
path but builds the href as `/teams/{id}`; change this to `/webhooks/{id}` so
the endpoint breadcrumb points to the correct resource. Update the branch that
sets href when match exists to construct `/webhooks/${match[1]}`, keep the
empty-string fallback, and return the breadcrumb array unchanged otherwise.

Comment on lines 296 to 322
const formSchema = yup.object({
email: yup.string()
.email("Please enter a valid e-mail address")
.defined("E-mail is required")
.label("E-mail")
.label(t('fields.email'))
.meta({
stackFormFieldPlaceholder: "Enter e-mail address",
stackFormFieldPlaceholder: t('fields.emailPlaceholder'),
}),
isVerified: yup.boolean()
.default(false)
.label("Set as verified")
.label(t('fields.isVerified'))
.meta({
description: "E-mails verified by verification emails. Can be used for OTP/magic links"
description: t('hints.isVerified')
}),
isPrimary: yup.boolean()
.default(false)
.label("Set as primary")
.label(t('fields.isPrimary'))
.meta({
description: "Make this the primary e-mail for the user"
description: t('hints.isPrimary')
}),
isUsedForAuth: yup.boolean()
.default(false)
.label("Used for sign-in")
.label(t('fields.isUsedForAuth'))
.meta({
description: "Allow this e-mail to be used for password sign-in. Also enables OTP/magic links if the e-mail is verified."
description: t('hints.isUsedForAuth')
}),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Translate Yup validation error messages.

The form schema contains hardcoded English validation error messages (.email("Please enter a valid e-mail address") and .defined("E-mail is required")). These should use translation keys for consistency with the i18n implementation.

Based on the pattern shown in apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx, validation messages should be translated. Apply this diff:

+  const tValidation = useTranslations('userDetail.contactChannels.addDialog.validation');
   const formSchema = yup.object({
     email: yup.string()
-      .email("Please enter a valid e-mail address")
-      .defined("E-mail is required")
+      .email(tValidation('invalidEmail'))
+      .defined(tValidation('emailRequired'))
       .label(t('fields.email'))

You'll need to add the corresponding keys to your translation files:

{
  "userDetail": {
    "contactChannels": {
      "addDialog": {
        "validation": {
          "invalidEmail": "Please enter a valid e-mail address",
          "emailRequired": "E-mail is required"
        }
      }
    }
  }
}
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
around lines 296 to 322, the Yup validators use hardcoded English messages;
replace .email("Please enter a valid e-mail address") with
.email(t('userDetail.contactChannels.addDialog.validation.invalidEmail')) and
.defined("E-mail is required") with
.defined(t('userDetail.contactChannels.addDialog.validation.emailRequired'));
keep the rest of the schema intact and add the two keys to your translation JSON
files as shown in the review comment.

Comment on lines +1 to +60
import { Card, Typography } from '@stackframe/stack-ui';
import { useTranslations } from 'next-intl';

export const metadata = {
title: "i18n Test",
};

export default function I18nTestPage() {
const t = useTranslations('common');
const tProjects = useTranslations('projects');
const tAuth = useTranslations('auth');

return (
<div className="container mx-auto p-8 space-y-6">
<Typography type="h1">i18n Test Page</Typography>

<Card className="p-6">
<Typography type="h2" className="mb-4">Common Translations</Typography>
<ul className="space-y-2">
<li>Dashboard: {t('dashboard')}</li>
<li>Projects: {t('projects')}</li>
<li>Settings: {t('settings')}</li>
<li>Save: {t('save')}</li>
<li>Cancel: {t('cancel')}</li>
<li>Loading: {t('loading')}</li>
</ul>
</Card>

<Card className="p-6">
<Typography type="h2" className="mb-4">Projects Translations</Typography>
<ul className="space-y-2">
<li>Title: {tProjects('title')}</li>
<li>New Project: {tProjects('newProject')}</li>
<li>Create Project: {tProjects('createProject')}</li>
</ul>
</Card>

<Card className="p-6">
<Typography type="h2" className="mb-4">Auth Translations</Typography>
<ul className="space-y-2">
<li>Sign In: {tAuth('signIn')}</li>
<li>Sign Up: {tAuth('signUp')}</li>
<li>Email: {tAuth('email')}</li>
<li>Password: {tAuth('password')}</li>
</ul>
</Card>

<Card className="p-6 bg-blue-50 dark:bg-blue-950">
<Typography type="h3" className="mb-2">How to Test</Typography>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Check the language switcher in the navbar (top right, globe icon 🌐)</li>
<li>Click to switch between English and 中文</li>
<li>Observe how the text on this page changes</li>
<li>Try switching languages: /i18n-test (English) vs /zh/i18n-test (Chinese)</li>
</ol>
</Card>
</div>
);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add "use client" directive to enable hooks.

This component uses useTranslations hooks (lines 9-11) but lacks the "use client" directive. In Next.js App Router, components are Server Components by default, and hooks can only be used in Client Components. Without this directive, the component will fail at runtime.

Apply this diff:

+"use client";
+
 import { Card, Typography } from '@stackframe/stack-ui';
 import { useTranslations } from 'next-intl';

Alternatively, if you want to keep this as a Server Component, replace useTranslations with the async getTranslations function:

 import { Card, Typography } from '@stackframe/stack-ui';
-import { useTranslations } from 'next-intl';
+import { getTranslations } from 'next-intl/server';

 export const metadata = {
   title: "i18n Test",
 };

-export default function I18nTestPage() {
-  const t = useTranslations('common');
-  const tProjects = useTranslations('projects');
-  const tAuth = useTranslations('auth');
+export default async function I18nTestPage() {
+  const t = await getTranslations('common');
+  const tProjects = await getTranslations('projects');
+  const tAuth = await getTranslations('auth');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { Card, Typography } from '@stackframe/stack-ui';
import { useTranslations } from 'next-intl';
export const metadata = {
title: "i18n Test",
};
export default function I18nTestPage() {
const t = useTranslations('common');
const tProjects = useTranslations('projects');
const tAuth = useTranslations('auth');
return (
<div className="container mx-auto p-8 space-y-6">
<Typography type="h1">i18n Test Page</Typography>
<Card className="p-6">
<Typography type="h2" className="mb-4">Common Translations</Typography>
<ul className="space-y-2">
<li>Dashboard: {t('dashboard')}</li>
<li>Projects: {t('projects')}</li>
<li>Settings: {t('settings')}</li>
<li>Save: {t('save')}</li>
<li>Cancel: {t('cancel')}</li>
<li>Loading: {t('loading')}</li>
</ul>
</Card>
<Card className="p-6">
<Typography type="h2" className="mb-4">Projects Translations</Typography>
<ul className="space-y-2">
<li>Title: {tProjects('title')}</li>
<li>New Project: {tProjects('newProject')}</li>
<li>Create Project: {tProjects('createProject')}</li>
</ul>
</Card>
<Card className="p-6">
<Typography type="h2" className="mb-4">Auth Translations</Typography>
<ul className="space-y-2">
<li>Sign In: {tAuth('signIn')}</li>
<li>Sign Up: {tAuth('signUp')}</li>
<li>Email: {tAuth('email')}</li>
<li>Password: {tAuth('password')}</li>
</ul>
</Card>
<Card className="p-6 bg-blue-50 dark:bg-blue-950">
<Typography type="h3" className="mb-2">How to Test</Typography>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Check the language switcher in the navbar (top right, globe icon 🌐)</li>
<li>Click to switch between English and 中文</li>
<li>Observe how the text on this page changes</li>
<li>Try switching languages: /i18n-test (English) vs /zh/i18n-test (Chinese)</li>
</ol>
</Card>
</div>
);
}
"use client";
import { Card, Typography } from '@stackframe/stack-ui';
import { useTranslations } from 'next-intl';
export const metadata = {
title: "i18n Test",
};
export default function I18nTestPage() {
const t = useTranslations('common');
const tProjects = useTranslations('projects');
const tAuth = useTranslations('auth');
return (
<div className="container mx-auto p-8 space-y-6">
<Typography type="h1">i18n Test Page</Typography>
<Card className="p-6">
<Typography type="h2" className="mb-4">Common Translations</Typography>
<ul className="space-y-2">
<li>Dashboard: {t('dashboard')}</li>
<li>Projects: {t('projects')}</li>
<li>Settings: {t('settings')}</li>
<li>Save: {t('save')}</li>
<li>Cancel: {t('cancel')}</li>
<li>Loading: {t('loading')}</li>
</ul>
</Card>
<Card className="p-6">
<Typography type="h2" className="mb-4">Projects Translations</Typography>
<ul className="space-y-2">
<li>Title: {tProjects('title')}</li>
<li>New Project: {tProjects('newProject')}</li>
<li>Create Project: {tProjects('createProject')}</li>
</ul>
</Card>
<Card className="p-6">
<Typography type="h2" className="mb-4">Auth Translations</Typography>
<ul className="space-y-2">
<li>Sign In: {tAuth('signIn')}</li>
<li>Sign Up: {tAuth('signUp')}</li>
<li>Email: {tAuth('email')}</li>
<li>Password: {tAuth('password')}</li>
</ul>
</Card>
<Card className="p-6 bg-blue-50 dark:bg-blue-950">
<Typography type="h3" className="mb-2">How to Test</Typography>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Check the language switcher in the navbar (top right, globe icon 🌐)</li>
<li>Click to switch between English and 中文</li>
<li>Observe how the text on this page changes</li>
<li>Try switching languages: /i18n-test (English) vs /zh/i18n-test (Chinese)</li>
</ol>
</Card>
</div>
);
}
🤖 Prompt for AI Agents
In apps/dashboard/src/app/(main)/i18n-test/page.tsx lines 1-60, the component
uses client-side hooks useTranslations but lacks the "use client" directive; add
the literal string "use client" as the very first line of the file so the
component is compiled as a Client Component and the hooks will work, or
alternatively convert the file to a Server Component by removing the hooks and
using the async getTranslations API instead (pick one approach and apply
consistently).

Comment on lines 72 to 76
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Status" />,
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle={t('status')} />,
cell: ({ row }) => <BadgeCell badges={[row.original.status]} />,
filterFn: standardFilterFn,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Localize status badge text

UI shows raw values ('valid', 'expired', 'revoked'). Display translated labels while keeping raw values for filtering.

   {
     accessorKey: "status",
     header: ({ column }) => <DataTableColumnHeader column={column} columnTitle={t('status')} />,
-    cell: ({ row }) => <BadgeCell badges={[row.original.status]} />,
+    cell: ({ row }) => <BadgeCell badges={[tStatus(row.original.status)]} />,
     filterFn: standardFilterFn,
   },

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/dashboard/src/components/data-table/api-key-table.tsx around lines 72 to
76, the status column currently renders raw status values ("valid", "expired",
"revoked") in the BadgeCell; change the cell renderer to show a localized label
while leaving the underlying raw value untouched for filtering. Use the i18n
translate function (t) to map row.original.status to a user-facing string (e.g.,
t(`status.${row.original.status}`) or similar key) and pass that translated
string into BadgeCell (badges=[translatedLabel]), keeping filterFn as is so
filtering still operates on the raw status value. Ensure you import/obtain the t
function in this file and handle any missing translation fallback (e.g., default
to the raw status).

Comment on lines +33 to 48
const t = useTranslations('common.dataTable.dialogs.removeUser');
return <ActionDialog
title
danger
open={props.open}
onOpenChange={props.onOpenChange}
okButton={{
label: "Remove user from team",
label: t('buttonLabel'),
onClick: async () => { await props.team.removeUser(props.user.id); }
}}
cancelButton
confirmText="I understand this will cause the user to lose access to the team."
confirmText={t('confirmText')}
>
{`Are you sure you want to remove the user "${props.user.displayName}" from the team "${props.team.displayName}"?`}
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{t('description', { userName: props.user.displayName ?? props.user.primaryEmail ?? props.user.id, teamName: props.team.displayName ?? props.team.id })}
</ActionDialog>;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Localize dialog title and Cancel label

ActionDialog defaults (“Alert”/“Confirmation”, “Cancel”) stay English. Pass translated title and cancel label.

-  const t = useTranslations('common.dataTable.dialogs.removeUser');
+  const t = useTranslations('common.dataTable.dialogs.removeUser');
+  const tCommon = useTranslations('common.actions');
   return <ActionDialog
-    title
+    title={t('title')}
     danger
     open={props.open}
     onOpenChange={props.onOpenChange}
     okButton={{
       label: t('buttonLabel'),
       onClick: async () => { await props.team.removeUser(props.user.id); }
     }}
-    cancelButton
+    cancelButton={{ label: tCommon('cancel') }}
     confirmText={t('confirmText')}
   >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const t = useTranslations('common.dataTable.dialogs.removeUser');
return <ActionDialog
title
danger
open={props.open}
onOpenChange={props.onOpenChange}
okButton={{
label: "Remove user from team",
label: t('buttonLabel'),
onClick: async () => { await props.team.removeUser(props.user.id); }
}}
cancelButton
confirmText="I understand this will cause the user to lose access to the team."
confirmText={t('confirmText')}
>
{`Are you sure you want to remove the user "${props.user.displayName}" from the team "${props.team.displayName}"?`}
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{t('description', { userName: props.user.displayName ?? props.user.primaryEmail ?? props.user.id, teamName: props.team.displayName ?? props.team.id })}
</ActionDialog>;
const t = useTranslations('common.dataTable.dialogs.removeUser');
const tCommon = useTranslations('common.actions');
return <ActionDialog
title={t('title')}
danger
open={props.open}
onOpenChange={props.onOpenChange}
okButton={{
label: t('buttonLabel'),
onClick: async () => { await props.team.removeUser(props.user.id); }
}}
cancelButton={{ label: tCommon('cancel') }}
confirmText={t('confirmText')}
>
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{t('description', {
userName: props.user.displayName ?? props.user.primaryEmail ?? props.user.id,
teamName: props.team.displayName ?? props.team.id
})}
</ActionDialog>;
🤖 Prompt for AI Agents
In apps/dashboard/src/components/data-table/team-member-table.tsx around lines
33 to 48, the ActionDialog is using default English title and Cancel label; pass
localized strings from the translations hook by setting the title prop to
t('title') and replacing cancelButton with an object that includes label:
t('cancelLabel') (e.g., cancelButton={{ label: t('cancelLabel') }}), keeping
existing okButton and other props unchanged.

Comment on lines +22 to +28
const handleLanguageChange = (newLocale: string) => {
// Set locale cookie (1 year expiry)
document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000; SameSite=Lax`;

// Refresh the page to apply new locale
router.refresh();
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add Secure flag to locale cookie in production.

The cookie is set without the Secure flag, which means it can be transmitted over unencrypted HTTP connections. While this is acceptable in development, production deployments should enforce HTTPS-only transmission.

Apply this change to conditionally add the Secure flag:

   const handleLanguageChange = (newLocale: string) => {
-    // Set locale cookie (1 year expiry)
-    document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000; SameSite=Lax`;
+    // Set locale cookie (1 year expiry)
+    const secure = window.location.protocol === 'https:' ? '; Secure' : '';
+    document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000; SameSite=Lax${secure}`;
 
     // Refresh the page to apply new locale
     router.refresh();
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleLanguageChange = (newLocale: string) => {
// Set locale cookie (1 year expiry)
document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000; SameSite=Lax`;
// Refresh the page to apply new locale
router.refresh();
};
const handleLanguageChange = (newLocale: string) => {
// Set locale cookie (1 year expiry)
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000; SameSite=Lax${secure}`;
// Refresh the page to apply new locale
router.refresh();
};
🤖 Prompt for AI Agents
In apps/dashboard/src/components/language-switcher.tsx around lines 22 to 28,
the locale cookie is set without the Secure flag; update the cookie string to
append "; Secure" when running in production/over HTTPS. Modify the handler to
compute a secureSuffix (e.g., process.env.NODE_ENV === 'production' ? '; Secure'
: '' or check window.location.protocol === 'https:') and include it in the
document.cookie assignment so the cookie becomes `NEXT_LOCALE=...; path=/;
max-age=31536000; SameSite=Lax${secureSuffix}` ensuring Secure is only added in
production/secure contexts.

Comment on lines +14 to +16
open -a Docker
echo " Waiting for Docker to start..."
sleep 15
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mac-only Docker start; add cross‑platform handling

open -a Docker works only on macOS. Add OS checks; on Linux try systemd; otherwise instruct user.

-    echo "   Starting Docker Desktop..."
-    open -a Docker
+    echo "   Starting Docker..."
+    if [[ "$OSTYPE" == "darwin"* ]]; then
+      open -a Docker
+    elif command -v systemctl >/dev/null 2>&1; then
+      sudo systemctl start docker || true
+    else
+      echo "   Please start Docker manually for your platform."
+    fi

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In scripts/start-dev.sh around lines 14–16, the script currently runs the
macOS-only command open -a Docker; replace this with an OS check (use
uname/OSTYPE): if macOS, keep open -a Docker; if Linux, attempt to start the
daemon with sudo systemctl start docker (and fall back to sudo service docker
start if systemctl not available); if neither, print a clear instruction for the
user to start Docker (e.g., on Windows ask them to launch Docker Desktop). After
attempting to start Docker, wait for the daemon to be ready by polling a
lightweight Docker command (docker info or docker ps) with a timeout/interval
instead of a fixed sleep, and exit with a helpful error if Docker never becomes
available.

Comment on lines +28 to +41
CONFLICTING_CONTAINERS=$(docker ps --format '{{.Names}}' | grep -v "^stack-dependencies" || true)
if [ ! -z "$CONFLICTING_CONTAINERS" ]; then
echo "⚠️ Found non-Stack containers that might conflict:"
echo "$CONFLICTING_CONTAINERS"
echo ""
read -p "Do you want to stop these containers? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "$CONFLICTING_CONTAINERS" | xargs docker stop 2>/dev/null || true
echo "✅ Stopped conflicting containers"
else
echo "⚠️ Continuing with existing containers (may cause port conflicts)"
fi
fi
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stopping all non-Stack containers is too broad; detect only port conflicts

Current logic may stop unrelated containers. Filter by containers publishing to dev ports before prompting.

-CONFLICTING_CONTAINERS=$(docker ps --format '{{.Names}}' | grep -v "^stack-dependencies" || true)
+DEV_PORTS_REGEX='(8100|8101|8102|8103|8104|8105|8106|8113|8116|8117|8118)'
+# Capture only containers binding to the ports above
+CONFLICTING_CONTAINERS=$(docker ps --format '{{.ID}} {{.Names}} {{.Ports}}' \
+  | awk -v re="$DEV_PORTS_REGEX" '$0 ~ re { print $2 }' || true)
@@
-    read -p "Do you want to stop these containers? (y/N): " -n 1 -r
+    if [ -t 0 ]; then
+      read -p "Do you want to stop these containers? (y/N): " -n 1 -r
+    else
+      REPLY="n"
+    fi
@@
-        echo "$CONFLICTING_CONTAINERS" | xargs docker stop 2>/dev/null || true
+        echo "$CONFLICTING_CONTAINERS" | xargs -r docker stop 2>/dev/null || true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CONFLICTING_CONTAINERS=$(docker ps --format '{{.Names}}' | grep -v "^stack-dependencies" || true)
if [ ! -z "$CONFLICTING_CONTAINERS" ]; then
echo "⚠️ Found non-Stack containers that might conflict:"
echo "$CONFLICTING_CONTAINERS"
echo ""
read -p "Do you want to stop these containers? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "$CONFLICTING_CONTAINERS" | xargs docker stop 2>/dev/null || true
echo "✅ Stopped conflicting containers"
else
echo "⚠️ Continuing with existing containers (may cause port conflicts)"
fi
fi
DEV_PORTS_REGEX='(8100|8101|8102|8103|8104|8105|8106|8113|8116|8117|8118)'
# Capture only containers binding to the ports above
CONFLICTING_CONTAINERS=$(docker ps --format '{{.ID}} {{.Names}} {{.Ports}}' \
| awk -v re="$DEV_PORTS_REGEX" '$0 ~ re { print $2 }' || true)
if [ ! -z "$CONFLICTING_CONTAINERS" ]; then
echo "⚠️ Found non-Stack containers that might conflict:"
echo "$CONFLICTING_CONTAINERS"
echo ""
if [ -t 0 ]; then
read -p "Do you want to stop these containers? (y/N): " -n 1 -r
else
REPLY="n"
fi
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "$CONFLICTING_CONTAINERS" | xargs -r docker stop 2>/dev/null || true
echo "✅ Stopped conflicting containers"
else
echo "⚠️ Continuing with existing containers (may cause port conflicts)"
fi
fi
🤖 Prompt for AI Agents
In scripts/start-dev.sh around lines 28-41, instead of stopping all non-Stack
containers, detect only containers that publish ports conflicting with our dev
ports: define a DEV_PORTS list (or read from env/compose), list running
containers with their published ports (docker ps --format '{{.Names}}
{{.Ports}}'), filter that output for any container whose published ports match
any port in DEV_PORTS, and assign those names to CONFLICTING_CONTAINERS; keep
the existing prompt/stop flow but operate only on this filtered set so unrelated
containers are not stopped.

@fuwenbin fuwenbin marked this pull request as draft October 10, 2025 10:35
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.

2 participants