Skip to content

Commit 1f0a581

Browse files
BruzWJagualdroncodex
authored
feat: promote staging localization and UI updates (#137)
* feat(i18n): add locale-aware routing and translate app copy Wire *next-intl* into app routing, request handling, layouts, and metadata generation. Move public, auth, admin, chat, invite, unsubscribe, workspace, and workflow widget copy into shared *en*, *es*, and *zh-CN* message catalogs. Localize navigation, auth callbacks, markdown and legal routes, blog metadata, and workflow tooling, and add audit or sync scripts plus regression tests for locale-aware behavior. * fix(i18n): localize landing previews and route handling Precompute localized landing workflow preview payloads and summary rows so the landing page can render workflow and market previews without loading the full workspace namespace. Apply localized routing and header handling for public app and API paths, localize invite acceptance and workspace file headers, and guard against missing workspace widget messages in scoped providers. Add regression coverage for preview rendering, scoped message providers, invite flows, workspace files, and localized Next header rules. * fix(workflow-editor): localize block, trigger, and inspector copy Localize workflow block chrome, condition branch labels, empty states, summary rows, and input/response field copy through shared workflow editor messages. Fix block and sub-block override resolution so built-in tools and trigger-only configs render translated titles, placeholders, descriptions, and option labels consistently. Add regression coverage for workflow blocks, condition inputs, node editor panels, read-only preview panels, and block-editor localization helpers. * fix(i18n): adapt restack to current staging * fix(auth): avoid double-localized redirects in auth flows Pass canonical internal paths to `@/i18n/navigation` in the signup, login, and verify flows so next-intl applies locale prefixes only once. This fixes redirects like `/es/es/verify` and `/zh/zh/verify` while keeping browser-level redirects and auth callback URLs locale-aware. Add regression coverage for signup, login, and verification navigation across `es` and `zh-CN`, and document the router contract in the i18n navigation helper. Tests: bun run test 'app/(auth)/auth-locale-redirects.test.tsx' 'app/(auth)/verify/use-verification.test.tsx' i18n/utils.test.ts * fix(i18n): rename Chinese locale contract to zh #133 Replace the app Chinese locale identifier with *zh* across routing, message catalogs, generated i18n assets, and locale-aware tests. Rename *apps/tradinggoose/i18n/messages/zh-CN.json* to *apps/tradinggoose/i18n/messages/zh.json* and regenerate *apps/tradinggoose/i18n.lock* from the updated *i18n.json* target list. Keep derived protocol outputs unchanged where regional values are still required, including Open Graph *zh_CN* and chat speech recognition *zh-CN*. Add the required dated staging changelog entry in *changelog/May-30-2026.md* for the branch diff against *origin/staging*. * fix(invite): preserve localized auth callbacks in invite flow #133 Localize the invite callback URL before encoding it for login and signup redirects so post-auth navigation returns users to the active locale. Add a signed-out invite flow test that asserts the localized callback URL and reset mocked session state between tests. * fix(search): remove unnecessary search parameter validation in market routes * fix(auth): improve localized sign-in and invite handling Co-authored-by: Codex <codex@openai.com> * refactor(workspace): unify MCP widget error handling Co-authored-by: Codex <codex@openai.com> * refactor(landing): remove newsletter form from cta Co-authored-by: Codex <codex@openai.com> * refactor(monitor): simplify board and control layout Remove redundant monitor board headers, aggregate badges, and chrome while keeping the board and control layouts functional. Co-authored-by: Codex <codex@openai.com> * fix(records): simplify execution price display Use the resolved fill price directly in order details instead of the shared execution price helper. Co-authored-by: Codex <codex@openai.com> * chore(i18n): remove unused monitor translation keys Drop monitor strings that are no longer referenced after simplifying the board and records views. Co-authored-by: Codex <codex@openai.com> * feat(listing): enhance asset class handling in search utilities and tests * feat(triggers): update trigger instructions and enhance deployment modal descriptions * fix(trigger): remove unused monitor fields from portfolio state trigger * fix(i18n): localize monitor, selector, and settings surfaces Replace remaining hardcoded English copy across the monitor editor, provider selectors, account settings, and help modal with localized workspace messages. Pass localized fallback labels into monitor workflow target loading and use centralized block editor copy for portfolio trigger metadata and instructions. Add translation entries and regression tests covering localized selector states, monitor editor labels, settings modal flows, and public copy shape validation. * feat(i18n): update Chinese localization for waitlist and login description feat(copilot): refactor workflow variable types and enhance type safety feat(blocks): improve subBlock description handling in block mermaid catalog test(blocks): add assertion for subBlock description in blocks metadata test * feat(i18n): localize app routes and copy plumbing Co-authored-by: Codex <codex@openai.com> * refactor(i18n): canonicalize internal route boundaries Co-authored-by: Codex <codex@openai.com> * feat(i18n): localize transactional emails and persist locale preferences Co-authored-by: Codex <codex@openai.com> * feat(i18n): persist anonymous email locales on waitlist Consolidate template formatting helpers into i18n/utils, move anonymous email locale persistence onto waitlist rows, and update the affected layouts, routes, tests, and generated Drizzle artifacts. Co-authored-by: Codex <codex@openai.com> * refactor(i18n): normalize localized copy and template keys Co-authored-by: Codex <codex@openai.com> * refactor(i18n): replace custom message hooks with next-intl Co-authored-by: Codex <codex@openai.com> * feat(i18n): update URLs to use 'www' subdomain and localize paths for email and auth redirects * feat(i18n): integrate next-intl for localization in admin and workspace layouts * refactor(layout): simplify intl hydration setup Remove legacy hydration logging, browser extension filtering, and unused client messages from the locale layout. Co-authored-by: Codex <codex@openai.com> * fix(settings): remove silent settings fallbacks Require authenticated settings updates, return real API errors, and stop skipping updates on the chat page. Co-authored-by: Codex <codex@openai.com> * build(test): upgrade vitest toolchain Co-authored-by: Codex <codex@openai.com> * feat(i18n): propagate canonical callback paths Co-authored-by: Codex <codex@openai.com> * feat(i18n): localize workflow labels and update related tests Co-authored-by: Codex <codex@openai.com> Co-authored-by: BWJ2310 <brucewj2310@gmail.com> * feat(tradinggoose): make locale-aware navigation state Co-authored-by: Codex <codex@openai.com> * fix(nav): remove unnecessary alignment property from DropdownMenuContent * feat(tests): enhance type safety for WatchlistListActionsButton tests * feat(i18n): update URLs to use SITE_BASE_URL for localization consistency * feat(i18n): enhance user settings localization and refactor SettingsLoader component * fix(copilot): localize workspace copilot widget copy Add dedicated *workspace.widgets.copilot* message trees for *en*, *es*, and *zh* and expose them through *useCopilotMessages*. Refactor copilot welcome, input, message chrome, todo list, access/model selectors, context usage, and chat history UI to consume localized copy instead of hardcoded English. Update the header to format relative time and history group labels from the active locale. Add regression coverage for the new copilot public copy and localized welcome rendering. * feat(auth): integrate session management in LanguageSwitcher component * feat(i18n): refactor locale handling in user settings API and improve default settings response * feat(i18n): enhance localization in layout, not-found, sitemap, and metadata handling * feat(i18n): implement workspace access check in metrics execution route and update related tests * feat(records): localize records dashboard and share filter metadata Co-authored-by: Codex <codex@openai.com> * feat(i18n): implement next-intl localization layer across app routes and components * feat(ui): add grouped sidebar dropdown menus Co-authored-by: Codex <codex@openai.com> * refactor(workflow): extract shared panel class name Co-authored-by: Codex <codex@openai.com> * feat(monitor): add kanban column header actions Co-authored-by: Codex <codex@openai.com> * style(ui): soften sidebar dropdown hover state Co-authored-by: Codex <codex@openai.com> * refactor(ui): optimize ComboBox option handling and cleanup unused dependencies --------- Co-authored-by: agualdron <agualdro@gmail.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: BWJ2310 <brucewj2310@gmail.com>
1 parent f6a7da4 commit 1f0a581

633 files changed

Lines changed: 104960 additions & 13071 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,5 @@ start-collector.sh
7373
## Helm Chart Tests
7474
helm/tradinggoose/test
7575
i18n.cache
76+
i18n.lock
7677
*.react-email
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
5+
import type React from 'react'
6+
import { act } from 'react'
7+
import { NextIntlClientProvider } from 'next-intl'
8+
import { createRoot, type Root } from 'react-dom/client'
9+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
10+
import { getPublicCopy } from '@/i18n/public-copy'
11+
import LoginPage from './login/login-form'
12+
import SignupPage from './signup/signup-form'
13+
import { VerifyContent } from './verify/verify-content'
14+
15+
const mockPush = vi.hoisted(() => vi.fn())
16+
const mockSignUpEmail = vi.hoisted(() => vi.fn())
17+
const mockSignInEmail = vi.hoisted(() => vi.fn())
18+
const mockSendVerificationOtp = vi.hoisted(() => vi.fn())
19+
const mockRefetchSession = vi.hoisted(() => vi.fn())
20+
const mockUseVerification = vi.hoisted(() => vi.fn())
21+
const mockFetch = vi.hoisted(() => vi.fn())
22+
const testState = vi.hoisted(() => ({
23+
searchParams: new URLSearchParams(),
24+
}))
25+
26+
vi.mock('next/navigation', () => ({
27+
useSearchParams: () => ({
28+
get: (key: string) => testState.searchParams.get(key),
29+
}),
30+
}))
31+
32+
vi.mock('@/i18n/navigation', () => ({
33+
Link: ({
34+
children,
35+
href,
36+
...props
37+
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & {
38+
children?: React.ReactNode
39+
href: string
40+
}) => (
41+
<a href={href} {...props}>
42+
{children}
43+
</a>
44+
),
45+
useRouter: () => ({
46+
push: mockPush,
47+
}),
48+
}))
49+
50+
vi.mock('@/lib/auth-client', () => ({
51+
client: {
52+
signUp: {
53+
email: mockSignUpEmail,
54+
},
55+
signIn: {
56+
email: mockSignInEmail,
57+
},
58+
emailOtp: {
59+
sendVerificationOtp: mockSendVerificationOtp,
60+
},
61+
},
62+
useSession: () => ({
63+
refetch: mockRefetchSession,
64+
}),
65+
}))
66+
67+
vi.mock('@/app/(auth)/verify/use-verification', () => ({
68+
useVerification: mockUseVerification,
69+
}))
70+
71+
vi.mock('@/app/(auth)/components/social-login-buttons', () => ({
72+
SocialLoginButtons: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
73+
}))
74+
75+
vi.mock('@/app/(auth)/components/sso-login-button', () => ({
76+
SSOLoginButton: () => null,
77+
}))
78+
79+
vi.mock('@/app/(auth)/components/auth-page-header', () => ({
80+
AuthPageHeader: () => null,
81+
}))
82+
83+
vi.mock('@/app/(auth)/components/auth-waitlist-note', () => ({
84+
AuthWaitlistNote: () => null,
85+
}))
86+
87+
vi.mock('@/app/fonts/inter', () => ({
88+
inter: { className: '' },
89+
}))
90+
91+
vi.mock('@/components/ui/button', () => ({
92+
Button: ({
93+
children,
94+
...props
95+
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
96+
children?: React.ReactNode
97+
}) => <button {...props}>{children}</button>,
98+
}))
99+
100+
vi.mock('@/components/ui/input', () => ({
101+
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
102+
}))
103+
104+
vi.mock('@/components/ui/label', () => ({
105+
Label: ({
106+
children,
107+
...props
108+
}: React.LabelHTMLAttributes<HTMLLabelElement> & {
109+
children?: React.ReactNode
110+
}) => <label {...props}>{children}</label>,
111+
}))
112+
113+
vi.mock('@/components/ui/dialog', () => ({
114+
Dialog: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
115+
DialogContent: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
116+
DialogDescription: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
117+
DialogHeader: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
118+
DialogTitle: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
119+
}))
120+
121+
vi.mock('@/components/ui/input-otp', () => ({
122+
InputOTP: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
123+
InputOTPGroup: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
124+
InputOTPSlot: ({ index }: { index: number }) => <div data-index={index} />,
125+
}))
126+
127+
vi.mock('@/lib/env', () => ({
128+
env: {
129+
NODE_ENV: 'test',
130+
EMAIL_VERIFICATION_ENABLED: false,
131+
},
132+
getEnv: vi.fn(() => undefined),
133+
isTruthy: vi.fn(() => false),
134+
}))
135+
136+
describe('auth locale redirects', () => {
137+
let container: HTMLDivElement
138+
let root: Root
139+
const reactActEnvironment = globalThis as typeof globalThis & {
140+
IS_REACT_ACT_ENVIRONMENT?: boolean
141+
}
142+
143+
beforeEach(() => {
144+
reactActEnvironment.IS_REACT_ACT_ENVIRONMENT = true
145+
container = document.createElement('div')
146+
document.body.appendChild(container)
147+
root = createRoot(container)
148+
testState.searchParams = new URLSearchParams()
149+
mockPush.mockReset()
150+
mockSignUpEmail.mockReset()
151+
mockSignInEmail.mockReset()
152+
mockSendVerificationOtp.mockReset()
153+
mockRefetchSession.mockReset()
154+
mockUseVerification.mockReset()
155+
mockFetch.mockReset()
156+
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }))
157+
global.fetch = mockFetch
158+
})
159+
160+
afterEach(() => {
161+
act(() => {
162+
root.unmount()
163+
})
164+
container.remove()
165+
vi.restoreAllMocks()
166+
reactActEnvironment.IS_REACT_ACT_ENVIRONMENT = false
167+
})
168+
169+
async function renderWithLocale(locale: 'en' | 'es' | 'zh', element: React.ReactElement) {
170+
await act(async () => {
171+
root.render(
172+
<NextIntlClientProvider locale={locale} messages={getPublicCopy(locale)}>
173+
{element}
174+
</NextIntlClientProvider>
175+
)
176+
})
177+
}
178+
179+
async function setInputValue(selector: string, value: string) {
180+
const input = container.querySelector(selector)
181+
182+
if (!(input instanceof HTMLInputElement)) {
183+
throw new Error(`Expected input ${selector} to render`)
184+
}
185+
186+
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
187+
valueSetter?.call(input, value)
188+
189+
await act(async () => {
190+
input.dispatchEvent(new Event('input', { bubbles: true }))
191+
input.dispatchEvent(new Event('change', { bubbles: true }))
192+
})
193+
}
194+
195+
async function submitRenderedForm() {
196+
const form = container.querySelector('form')
197+
198+
if (!(form instanceof HTMLFormElement)) {
199+
throw new Error('Expected auth form to render')
200+
}
201+
202+
await act(async () => {
203+
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
204+
})
205+
}
206+
207+
it.each(['es', 'zh'] as const)(
208+
'pushes the canonical verify path after signup for %s',
209+
async (locale) => {
210+
mockSignUpEmail.mockResolvedValue({ user: { id: 'user-1' } })
211+
mockRefetchSession.mockResolvedValue(undefined)
212+
mockSendVerificationOtp.mockResolvedValue(undefined)
213+
214+
await renderWithLocale(
215+
locale,
216+
<SignupPage
217+
githubAvailable={false}
218+
googleAvailable={false}
219+
isProduction={false}
220+
registrationMode='open'
221+
/>
222+
)
223+
224+
await setInputValue('#name', 'Ada Lovelace')
225+
await setInputValue('#email', 'ada@example.com')
226+
await setInputValue('#password', 'Password1!')
227+
await submitRenderedForm()
228+
229+
expect(mockPush).toHaveBeenCalledWith('/verify?fromSignup=true')
230+
}
231+
)
232+
233+
it.each(['es', 'zh'] as const)(
234+
'pushes the canonical verify path after an unverified login for %s',
235+
async (locale) => {
236+
mockSignInEmail.mockRejectedValue({ code: 'EMAIL_NOT_VERIFIED' })
237+
238+
await renderWithLocale(
239+
locale,
240+
<LoginPage
241+
githubAvailable={false}
242+
googleAvailable={false}
243+
isProduction={false}
244+
registrationMode='open'
245+
/>
246+
)
247+
248+
await setInputValue('#email', 'ada@example.com')
249+
await setInputValue('#password', 'Password1!')
250+
await submitRenderedForm()
251+
252+
expect(mockPush).toHaveBeenCalledWith('/verify')
253+
}
254+
)
255+
256+
it('pushes the canonical signup path from the verify screen back action', async () => {
257+
mockUseVerification.mockReturnValue({
258+
otp: '',
259+
email: 'ada@example.com',
260+
isLoading: false,
261+
isVerified: false,
262+
isInvalidOtp: false,
263+
errorMessage: '',
264+
isOtpComplete: false,
265+
hasEmailService: true,
266+
isProduction: false,
267+
isEmailVerificationEnabled: true,
268+
verifyCode: vi.fn(),
269+
resendCode: vi.fn(),
270+
handleOtpChange: vi.fn(),
271+
})
272+
273+
await renderWithLocale(
274+
'en',
275+
<VerifyContent
276+
hasEmailService
277+
isProduction={false}
278+
isEmailVerificationEnabled
279+
/>
280+
)
281+
282+
const backButton = Array.from(container.querySelectorAll('button')).find(
283+
(button) => button.textContent === getPublicCopy('en').auth.common.backToSignup
284+
)
285+
286+
if (!(backButton instanceof HTMLButtonElement)) {
287+
throw new Error('Expected back to signup button to render')
288+
}
289+
290+
await act(async () => {
291+
backButton.click()
292+
})
293+
294+
expect(mockPush).toHaveBeenCalledWith('/signup')
295+
})
296+
})
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1+
'use client'
2+
3+
import { useLocale } from 'next-intl'
14
import { inter } from '@/app/fonts/inter'
5+
import { useMessages } from 'next-intl'
6+
import { type LocaleCode } from '@/i18n/utils'
27

38
export function AuthWaitlistNote() {
9+
const locale = useLocale() as LocaleCode
10+
const copy = useMessages()
11+
412
return (
513
<div
614
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`}
715
>
8-
Use the same waitlist-approved email for any sign-in method.
16+
{copy.auth.note.waitlistApprovedEmail}
917
</div>
1018
)
1119
}

0 commit comments

Comments
 (0)