Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a152b30
feat(i18n): add locale-aware routing and translate app copy
agualdron May 28, 2026
f98181d
fix(i18n): localize landing previews and route handling
agualdron May 29, 2026
96569f7
fix(workflow-editor): localize block, trigger, and inspector copy
agualdron May 29, 2026
cd0f4f8
fix(i18n): adapt restack to current staging
agualdron May 31, 2026
3cb88e9
fix(auth): avoid double-localized redirects in auth flows
agualdron May 31, 2026
effcc4c
fix(i18n): rename Chinese locale contract to zh #133
agualdron May 31, 2026
e75e5b0
fix(invite): preserve localized auth callbacks in invite flow #133
agualdron May 31, 2026
3512fa1
fix(search): remove unnecessary search parameter validation in market…
BWJ2310-backup May 31, 2026
3b22124
fix(auth): improve localized sign-in and invite handling
BWJ2310-backup May 31, 2026
c3cf2b9
refactor(workspace): unify MCP widget error handling
BWJ2310-backup May 31, 2026
5645ff0
refactor(landing): remove newsletter form from cta
BWJ2310-backup May 31, 2026
6d9a335
refactor(monitor): simplify board and control layout
BWJ2310-backup May 31, 2026
021447e
fix(records): simplify execution price display
BWJ2310-backup May 31, 2026
8159ec3
chore(i18n): remove unused monitor translation keys
BWJ2310-backup May 31, 2026
9bac772
feat(listing): enhance asset class handling in search utilities and t…
BWJ2310-backup May 31, 2026
de29b7c
feat(triggers): update trigger instructions and enhance deployment mo…
BWJ2310-backup Jun 1, 2026
e1b4db1
fix(trigger): remove unused monitor fields from portfolio state trigger
BWJ2310-backup Jun 1, 2026
7690943
fix(i18n): localize monitor, selector, and settings surfaces
agualdron Jun 1, 2026
d78e319
feat(i18n): update Chinese localization for waitlist and login descri…
BWJ2310-backup Jun 1, 2026
a1ed630
feat(i18n): localize app routes and copy plumbing
BWJ2310-backup Jun 2, 2026
6e55cc6
refactor(i18n): canonicalize internal route boundaries
BWJ2310-backup Jun 2, 2026
52b4d36
feat(i18n): localize transactional emails and persist locale preferences
BWJ2310-backup Jun 2, 2026
6412987
feat(i18n): persist anonymous email locales on waitlist
BWJ2310-backup Jun 2, 2026
ebc2353
refactor(i18n): normalize localized copy and template keys
BWJ2310-backup Jun 2, 2026
75197a3
refactor(i18n): replace custom message hooks with next-intl
BWJ2310-backup Jun 3, 2026
7a32202
feat(i18n): update URLs to use 'www' subdomain and localize paths for…
BWJ2310-backup Jun 3, 2026
9cd3d32
feat(i18n): integrate next-intl for localization in admin and workspa…
BWJ2310-backup Jun 3, 2026
1e12799
refactor(layout): simplify intl hydration setup
BWJ2310-backup Jun 3, 2026
e8b9dd3
fix(settings): remove silent settings fallbacks
BWJ2310-backup Jun 3, 2026
d692501
build(test): upgrade vitest toolchain
BWJ2310-backup Jun 3, 2026
4672055
feat(i18n): propagate canonical callback paths
BWJ2310-backup Jun 3, 2026
feff924
feat(i18n): localize workflow labels and update related tests
BWJ2310-backup Jun 3, 2026
7abdb1e
feat(tradinggoose): make locale-aware navigation state
BWJ2310-backup Jun 4, 2026
a5a6129
fix(nav): remove unnecessary alignment property from DropdownMenuContent
BWJ2310-backup Jun 4, 2026
542888d
feat(tests): enhance type safety for WatchlistListActionsButton tests
BWJ2310-backup Jun 4, 2026
6c875ae
feat(i18n): update URLs to use SITE_BASE_URL for localization consist…
BWJ2310-backup Jun 4, 2026
1d54b0c
feat(i18n): enhance user settings localization and refactor SettingsL…
BWJ2310-backup Jun 4, 2026
e27a5e7
fix(copilot): localize workspace copilot widget copy
agualdron Jun 5, 2026
0798911
feat(auth): integrate session management in LanguageSwitcher component
BWJ2310-backup Jun 5, 2026
7575519
feat(i18n): refactor locale handling in user settings API and improve…
BWJ2310-backup Jun 5, 2026
b5a1640
feat(i18n): enhance localization in layout, not-found, sitemap, and m…
BWJ2310-backup Jun 5, 2026
fe76232
feat(i18n): implement workspace access check in metrics execution rou…
BWJ2310-backup Jun 5, 2026
06bbaf4
feat(records): localize records dashboard and share filter metadata
BWJ2310-backup Jun 5, 2026
8aa5fe0
feat(i18n): implement next-intl localization layer across app routes …
BWJ2310-backup Jun 5, 2026
a2eb71b
Merge pull request #135 from TradingGoose/feat/i18n
BWJ2310-backup Jun 5, 2026
2ea5b10
feat(ui): add grouped sidebar dropdown menus
BWJ2310-backup Jun 3, 2026
d84530c
refactor(workflow): extract shared panel class name
BWJ2310-backup Jun 3, 2026
c0d4a7f
feat(monitor): add kanban column header actions
BWJ2310-backup Jun 5, 2026
3dfe77a
style(ui): soften sidebar dropdown hover state
BWJ2310-backup Jun 5, 2026
0ef1230
refactor(ui): optimize ComboBox option handling and cleanup unused de…
BWJ2310-backup Jun 5, 2026
e167f5f
Merge pull request #136 from TradingGoose/feat/dropdown-ui
BWJ2310-backup Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,5 @@ start-collector.sh
## Helm Chart Tests
helm/tradinggoose/test
i18n.cache
i18n.lock
*.react-email
296 changes: 296 additions & 0 deletions apps/tradinggoose/app/(auth)/auth-locale-redirects.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
/**
* @vitest-environment jsdom
*/

import type React from 'react'
import { act } from 'react'
import { NextIntlClientProvider } from 'next-intl'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getPublicCopy } from '@/i18n/public-copy'
import LoginPage from './login/login-form'
import SignupPage from './signup/signup-form'
import { VerifyContent } from './verify/verify-content'

const mockPush = vi.hoisted(() => vi.fn())
const mockSignUpEmail = vi.hoisted(() => vi.fn())
const mockSignInEmail = vi.hoisted(() => vi.fn())
const mockSendVerificationOtp = vi.hoisted(() => vi.fn())
const mockRefetchSession = vi.hoisted(() => vi.fn())
const mockUseVerification = vi.hoisted(() => vi.fn())
const mockFetch = vi.hoisted(() => vi.fn())
const testState = vi.hoisted(() => ({
searchParams: new URLSearchParams(),
}))

vi.mock('next/navigation', () => ({
useSearchParams: () => ({
get: (key: string) => testState.searchParams.get(key),
}),
}))

vi.mock('@/i18n/navigation', () => ({
Link: ({
children,
href,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & {
children?: React.ReactNode
href: string
}) => (
<a href={href} {...props}>
{children}
</a>
),
useRouter: () => ({
push: mockPush,
}),
}))

vi.mock('@/lib/auth-client', () => ({
client: {
signUp: {
email: mockSignUpEmail,
},
signIn: {
email: mockSignInEmail,
},
emailOtp: {
sendVerificationOtp: mockSendVerificationOtp,
},
},
useSession: () => ({
refetch: mockRefetchSession,
}),
}))

vi.mock('@/app/(auth)/verify/use-verification', () => ({
useVerification: mockUseVerification,
}))

vi.mock('@/app/(auth)/components/social-login-buttons', () => ({
SocialLoginButtons: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
}))

vi.mock('@/app/(auth)/components/sso-login-button', () => ({
SSOLoginButton: () => null,
}))

vi.mock('@/app/(auth)/components/auth-page-header', () => ({
AuthPageHeader: () => null,
}))

vi.mock('@/app/(auth)/components/auth-waitlist-note', () => ({
AuthWaitlistNote: () => null,
}))

vi.mock('@/app/fonts/inter', () => ({
inter: { className: '' },
}))

vi.mock('@/components/ui/button', () => ({
Button: ({
children,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
children?: React.ReactNode
}) => <button {...props}>{children}</button>,
}))

vi.mock('@/components/ui/input', () => ({
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
}))

vi.mock('@/components/ui/label', () => ({
Label: ({
children,
...props
}: React.LabelHTMLAttributes<HTMLLabelElement> & {
children?: React.ReactNode
}) => <label {...props}>{children}</label>,
}))

vi.mock('@/components/ui/dialog', () => ({
Dialog: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
DialogContent: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
DialogDescription: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
DialogHeader: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
DialogTitle: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
}))

vi.mock('@/components/ui/input-otp', () => ({
InputOTP: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
InputOTPGroup: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
InputOTPSlot: ({ index }: { index: number }) => <div data-index={index} />,
}))

vi.mock('@/lib/env', () => ({
env: {
NODE_ENV: 'test',
EMAIL_VERIFICATION_ENABLED: false,
},
getEnv: vi.fn(() => undefined),
isTruthy: vi.fn(() => false),
}))

describe('auth locale redirects', () => {
let container: HTMLDivElement
let root: Root
const reactActEnvironment = globalThis as typeof globalThis & {
IS_REACT_ACT_ENVIRONMENT?: boolean
}

beforeEach(() => {
reactActEnvironment.IS_REACT_ACT_ENVIRONMENT = true
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
testState.searchParams = new URLSearchParams()
mockPush.mockReset()
mockSignUpEmail.mockReset()
mockSignInEmail.mockReset()
mockSendVerificationOtp.mockReset()
mockRefetchSession.mockReset()
mockUseVerification.mockReset()
mockFetch.mockReset()
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }))
global.fetch = mockFetch
})

afterEach(() => {
act(() => {
root.unmount()
})
container.remove()
vi.restoreAllMocks()
reactActEnvironment.IS_REACT_ACT_ENVIRONMENT = false
})

async function renderWithLocale(locale: 'en' | 'es' | 'zh', element: React.ReactElement) {
await act(async () => {
root.render(
<NextIntlClientProvider locale={locale} messages={getPublicCopy(locale)}>
{element}
</NextIntlClientProvider>
)
})
}

async function setInputValue(selector: string, value: string) {
const input = container.querySelector(selector)

if (!(input instanceof HTMLInputElement)) {
throw new Error(`Expected input ${selector} to render`)
}

const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
valueSetter?.call(input, value)

await act(async () => {
input.dispatchEvent(new Event('input', { bubbles: true }))
input.dispatchEvent(new Event('change', { bubbles: true }))
})
}

async function submitRenderedForm() {
const form = container.querySelector('form')

if (!(form instanceof HTMLFormElement)) {
throw new Error('Expected auth form to render')
}

await act(async () => {
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
})
}

it.each(['es', 'zh'] as const)(
'pushes the canonical verify path after signup for %s',
async (locale) => {
mockSignUpEmail.mockResolvedValue({ user: { id: 'user-1' } })
mockRefetchSession.mockResolvedValue(undefined)
mockSendVerificationOtp.mockResolvedValue(undefined)

await renderWithLocale(
locale,
<SignupPage
githubAvailable={false}
googleAvailable={false}
isProduction={false}
registrationMode='open'
/>
)

await setInputValue('#name', 'Ada Lovelace')
await setInputValue('#email', 'ada@example.com')
await setInputValue('#password', 'Password1!')
await submitRenderedForm()

expect(mockPush).toHaveBeenCalledWith('/verify?fromSignup=true')
}
)

it.each(['es', 'zh'] as const)(
'pushes the canonical verify path after an unverified login for %s',
async (locale) => {
mockSignInEmail.mockRejectedValue({ code: 'EMAIL_NOT_VERIFIED' })

await renderWithLocale(
locale,
<LoginPage
githubAvailable={false}
googleAvailable={false}
isProduction={false}
registrationMode='open'
/>
)

await setInputValue('#email', 'ada@example.com')
await setInputValue('#password', 'Password1!')
await submitRenderedForm()

expect(mockPush).toHaveBeenCalledWith('/verify')
}
)

it('pushes the canonical signup path from the verify screen back action', async () => {
mockUseVerification.mockReturnValue({
otp: '',
email: 'ada@example.com',
isLoading: false,
isVerified: false,
isInvalidOtp: false,
errorMessage: '',
isOtpComplete: false,
hasEmailService: true,
isProduction: false,
isEmailVerificationEnabled: true,
verifyCode: vi.fn(),
resendCode: vi.fn(),
handleOtpChange: vi.fn(),
})

await renderWithLocale(
'en',
<VerifyContent
hasEmailService
isProduction={false}
isEmailVerificationEnabled
/>
)

const backButton = Array.from(container.querySelectorAll('button')).find(
(button) => button.textContent === getPublicCopy('en').auth.common.backToSignup
)

if (!(backButton instanceof HTMLButtonElement)) {
throw new Error('Expected back to signup button to render')
}

await act(async () => {
backButton.click()
})

expect(mockPush).toHaveBeenCalledWith('/signup')
})
})
10 changes: 9 additions & 1 deletion apps/tradinggoose/app/(auth)/components/auth-waitlist-note.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
'use client'

import { useLocale } from 'next-intl'
import { inter } from '@/app/fonts/inter'
import { useMessages } from 'next-intl'
import { type LocaleCode } from '@/i18n/utils'

export function AuthWaitlistNote() {
const locale = useLocale() as LocaleCode
const copy = useMessages()

return (
<div
className={`${inter.className} mx-auto mt-4 w-fit max-w-full rounded-md border bg-muted/30 px-4 py-3 text-center text-sm`}
>
Use the same waitlist-approved email for any sign-in method.
{copy.auth.note.waitlistApprovedEmail}
</div>
)
}
Loading
Loading