-
Notifications
You must be signed in to change notification settings - Fork 474
dashboard i18n #942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
dashboard i18n #942
Conversation
…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
|
|
WalkthroughAdds 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
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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
| "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", |
There was a problem hiding this comment.
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.
| "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\"", |
There was a problem hiding this 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 |
There was a problem hiding this 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:
- Visit dashboard without existing locale cookie
- Check network requests - no
x-next-intl-localeheader is attached - 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.
There was a problem hiding this 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 shippingDelete 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 alertsThese flows return
"prevent-close"but surface failures only viatoast({ 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
emailThemesnamespace 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
NewTemplateButtonfunction 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 insteadThis 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>} <TeamMemberSearchTableapps/dashboard/src/components/data-table/team-member-table.tsx (1)
75-94: Localize Cancel label in EditPermissionDialogEnsure 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
projectFormSchemais recreated on every component render, which is inefficient. Since the schema depends on the translation functiont, wrap it inuseMemowithtas 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 keepingtHints('emailVerifiedRequired')for the UI hint (separate keys with the same text in translation files), or- Moving this to
tErrorsand 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 functiont.The callback parameter in
teams.find(t => ...)on line 256 shadows the translation functiont. 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:
- Translation quality verification: Have a native Chinese speaker review technical terminology and UI strings for naturalness and accuracy.
- 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
useRouterfromnext/navigationwith 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 anytype assertions when checking if the locale is included inrouting.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 fiapps/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 aglobe.errorMessagekey atoverview.metrics.globe.errorMessagein en.json, you can simplify this tot('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 ]; thenpackages/stack-ui/src/components/data-table/pagination.tsx (1)
13-17: Solid i18n extension with safe defaults; consider minor a11y tweakProps 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/timeLeverage 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 remainMultiple 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 translationsAvoid 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 executionPrevents 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_treeAvoids 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 PIDsBlind 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 variableSC2034: 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 trapsPrevents 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 scriptWith 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 +fiapps/dashboard/src/components/data-table/api-key-table.tsx (1)
12-22: Consider i18n for DataTableFacetedFilter internal stringsFaceted 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
DataTableToolbarandDataTablePagination. However, the hardcoded "No results." string at line 119 is not exposed through the i18n interface.To fully internationalize the component, add a
noResultsfield toDataTableI18n: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", |
There was a problem hiding this comment.
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' })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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 }, | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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') | ||
| }), | ||
| }); |
There was a problem hiding this comment.
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.
| 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> | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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).
| 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, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
| 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>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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(); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| open -a Docker | ||
| echo " Waiting for Docker to start..." | ||
| sleep 15 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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."
+ fiCommittable 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.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
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-intlfor 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
apps/dashboard/src/i18n/routing.tsapps/dashboard/src/i18n/request.tsapps/dashboard/messages/en.jsonapps/dashboard/messages/zh.jsonapps/dashboard/next.config.mjsapps/dashboard/package.jsonapps/dashboard/src/app/layout.tsxapps/dashboard/src/components/language-switcher.tsxapps/dashboard/src/components/navbar.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/api-keys/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/transactions/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-permissions/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-permissions/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsxapps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsxapps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsxapps/dashboard/src/components/user-dialog.tsxapps/dashboard/src/components/data-table/user-table.tsxapps/dashboard/src/components/data-table/team-table.tsxapps/dashboard/src/components/data-table/team-member-table.tsxapps/dashboard/src/components/data-table/api-key-table.tsxapps/dashboard/src/components/data-table/permission-table.tsxapps/dashboard/src/components/data-table/transaction-table.tsxapps/dashboard/src/components/data-table/payment-product-table.tsxapps/dashboard/src/components/data-table/payment-item-table.tsxpackages/stack-ui/src/components/data-table/data-table.tsxpackages/stack-ui/src/components/data-table/pagination.tsxpackages/stack-ui/src/components/data-table/toolbar.tsxpackages/stack-ui/src/components/data-table/view-options.tsxapps/dashboard/src/app/(main)/i18n-test/page.tsxdocker/dependencies/README.mddocker/dependencies/docker.compose.minimal.yamlscripts/restart-dev-basic.shscripts/kill-dev-servers.shscripts/check-docker.shscripts/fix-docker.shscripts/start-dev.shpackage.jsonpnpm-lock.yamldocker/dependencies/README.mddocker/dependencies/docker.compose.minimal.yamlscripts/restart-dev-basic.shscripts/kill-dev-servers.shscripts/check-docker.shscripts/fix-docker.shscripts/start-dev.shpackage.jsonImportant
Adds internationalization support to the Stack Auth dashboard with English and Chinese languages, and includes Docker management scripts and development server utilities.
next-intlfor managing translations innext.config.mjs.language-switcher.tsxand integrates it intonavbar.tsxandsidebar-layout.tsx.useTranslationsfor text, e.g.,project-settings/page-client.tsx,users/page-client.tsx,i18n-test/page.tsx.en.jsonandzh.jsonwith over 1000+ strings for translation.routing.tsand request handling inrequest.tsfor locale management.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).package.jsonwith new npm scripts for Docker and development management.This description was created by
for aa2321b. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
New Features
Documentation
Chores