fix(web): localize error boundary copy (#34332)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
yyh
2026-03-31 16:41:20 +08:00
committed by GitHub
parent f27d669f87
commit b54a0dc1e4
4 changed files with 71 additions and 16 deletions

View File

@@ -1,6 +1,7 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createReactI18nextMock } from '@/test/i18n-mock'
import ErrorBoundary, { ErrorFallback, useAsyncError, useErrorHandler, withErrorBoundary } from '../index'
const mockConfig = vi.hoisted(() => ({
@@ -13,6 +14,19 @@ vi.mock('@/config', () => ({
},
}))
vi.mock('react-i18next', () => createReactI18nextMock({
'error': 'Error',
'errorBoundary.componentStack': 'Component Stack:',
'errorBoundary.details': 'Error Details (Development Only)',
'errorBoundary.errorCount': 'This error has occurred {{count}} times',
'errorBoundary.fallbackTitle': 'Oops! Something went wrong',
'errorBoundary.message': 'An unexpected error occurred while rendering this component.',
'errorBoundary.reloadPage': 'Reload Page',
'errorBoundary.title': 'Something went wrong',
'errorBoundary.tryAgain': 'Try Again',
'errorBoundary.tryAgainCompact': 'Try again',
}))
type ThrowOnRenderProps = {
message?: string
shouldThrow: boolean

View File

@@ -3,6 +3,7 @@ import type { ErrorInfo, ReactNode } from 'react'
import { RiAlertLine, RiBugLine } from '@remixicon/react'
import * as React from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { IS_DEV } from '@/config'
import { cn } from '@/utils/classnames'
@@ -29,9 +30,21 @@ type ErrorBoundaryProps = {
customMessage?: string
}
type ErrorBoundaryCopy = {
componentStack: string
details: string
error: string
formatErrorCount: (count: number) => string
message: string
reload: string
title: string
tryAgain: string
}
// Internal class component for error catching
class ErrorBoundaryInner extends React.Component<
ErrorBoundaryProps & {
copy: ErrorBoundaryCopy
resetErrorBoundary: () => void
onResetKeysChange: (prevResetKeys?: Array<string | number>) => void
},
@@ -96,6 +109,7 @@ class ErrorBoundaryInner extends React.Component<
enableRecovery = true,
customTitle,
customMessage,
copy,
resetErrorBoundary,
} = this.props
@@ -118,12 +132,12 @@ class ErrorBoundaryInner extends React.Component<
<div className="mb-4 flex items-center gap-2">
<RiAlertLine className="text-state-critical-solid h-8 w-8" />
<h2 className="text-xl font-semibold text-text-primary">
{customTitle || 'Something went wrong'}
{customTitle || copy.title}
</h2>
</div>
<p className="mb-6 text-center text-text-secondary">
{customMessage || 'An unexpected error occurred while rendering this component.'}
{customMessage || copy.message}
</p>
{showDetails && errorInfo && (
@@ -131,19 +145,19 @@ class ErrorBoundaryInner extends React.Component<
<summary className="mb-2 cursor-pointer text-sm font-medium text-text-tertiary hover:text-text-secondary">
<span className="inline-flex items-center gap-1">
<RiBugLine className="h-4 w-4" />
Error Details (Development Only)
{copy.details}
</span>
</summary>
<div className="rounded-lg bg-gray-100 p-4">
<div className="mb-2">
<span className="font-mono text-xs font-semibold text-gray-600">Error:</span>
<span className="font-mono text-xs font-semibold text-gray-600">{copy.error}</span>
<pre className="mt-1 overflow-auto whitespace-pre-wrap font-mono text-xs text-gray-800">
{error.toString()}
</pre>
</div>
{errorInfo && (
<div>
<span className="font-mono text-xs font-semibold text-gray-600">Component Stack:</span>
<span className="font-mono text-xs font-semibold text-gray-600">{copy.componentStack}</span>
<pre className="mt-1 max-h-40 overflow-auto whitespace-pre-wrap font-mono text-xs text-gray-700">
{errorInfo.componentStack}
</pre>
@@ -151,11 +165,7 @@ class ErrorBoundaryInner extends React.Component<
)}
{errorCount > 1 && (
<div className="mt-2 text-xs text-gray-600">
This error has occurred
{' '}
{errorCount}
{' '}
times
{copy.formatErrorCount(errorCount)}
</div>
)}
</div>
@@ -169,14 +179,14 @@ class ErrorBoundaryInner extends React.Component<
size="small"
onClick={resetErrorBoundary}
>
Try Again
{copy.tryAgain}
</Button>
<Button
variant="secondary"
size="small"
onClick={() => window.location.reload()}
>
Reload Page
{copy.reload}
</Button>
</div>
)}
@@ -190,9 +200,20 @@ class ErrorBoundaryInner extends React.Component<
// Main functional component wrapper
const ErrorBoundary: React.FC<ErrorBoundaryProps> = (props) => {
const { t } = useTranslation()
const [errorBoundaryKey, setErrorBoundaryKey] = useState(0)
const resetKeysRef = useRef(props.resetKeys)
const prevResetKeysRef = useRef<Array<string | number> | undefined>(undefined)
const copy = {
componentStack: t('errorBoundary.componentStack', { ns: 'common' }),
details: t('errorBoundary.details', { ns: 'common' }),
error: `${t('error', { ns: 'common' })}:`,
formatErrorCount: (count: number) => t('errorBoundary.errorCount', { ns: 'common', count }),
message: t('errorBoundary.message', { ns: 'common' }),
reload: t('errorBoundary.reloadPage', { ns: 'common' }),
title: t('errorBoundary.title', { ns: 'common' }),
tryAgain: t('errorBoundary.tryAgain', { ns: 'common' }),
}
const resetErrorBoundary = useCallback(() => {
setErrorBoundaryKey(prev => prev + 1)
@@ -211,6 +232,7 @@ const ErrorBoundary: React.FC<ErrorBoundaryProps> = (props) => {
return (
<ErrorBoundaryInner
{...props}
copy={copy}
key={errorBoundaryKey}
resetErrorBoundary={resetErrorBoundary}
onResetKeysChange={onResetKeysChange}
@@ -265,12 +287,14 @@ export const ErrorFallback: React.FC<{
error: Error
resetErrorBoundaryAction: () => void
}> = ({ error, resetErrorBoundaryAction }) => {
const { t } = useTranslation()
return (
<div className="flex min-h-[200px] flex-col items-center justify-center rounded-lg border border-red-200 bg-red-50 p-8">
<h2 className="mb-2 text-lg font-semibold text-red-800">Oops! Something went wrong</h2>
<h2 className="mb-2 text-lg font-semibold text-red-800">{t('errorBoundary.fallbackTitle', { ns: 'common' })}</h2>
<p className="mb-4 text-center text-red-600">{error.message}</p>
<Button onClick={resetErrorBoundaryAction} size="small">
Try again
{t('errorBoundary.tryAgainCompact', { ns: 'common' })}
</Button>
</div>
)

View File

@@ -3,9 +3,17 @@ import type { TriggerSubscription } from '@/app/components/workflow/block-select
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
import { createReactI18nextMock } from '@/test/i18n-mock'
import { SubscriptionList } from '../index'
import { SubscriptionListMode } from '../types'
vi.mock('react-i18next', () => createReactI18nextMock({
'errorBoundary.title': 'Something went wrong',
'errorBoundary.message': 'An unexpected error occurred while rendering this component.',
'errorBoundary.tryAgain': 'Try Again',
'errorBoundary.reloadPage': 'Reload Page',
}))
const mockRefetch = vi.fn()
let mockSubscriptionListError: Error | null = null
let mockSubscriptionListState: {
@@ -209,12 +217,12 @@ describe('SubscriptionList', () => {
})
describe('Edge Cases', () => {
it('should render error boundary fallback when an error occurs', () => {
it('should render error boundary fallback when an error occurs', async () => {
mockSubscriptionListError = new Error('boom')
render(<SubscriptionList />)
expect(screen.getByText('Something went wrong')).toBeInTheDocument()
expect(await screen.findByText('Something went wrong')).toBeInTheDocument()
})
})
})

View File

@@ -162,6 +162,15 @@
"environment.development": "DEVELOPMENT",
"environment.testing": "TESTING",
"error": "Error",
"errorBoundary.componentStack": "Component Stack:",
"errorBoundary.details": "Error Details (Development Only)",
"errorBoundary.errorCount": "This error has occurred {{count}} times",
"errorBoundary.fallbackTitle": "Oops! Something went wrong",
"errorBoundary.message": "An unexpected error occurred while rendering this component.",
"errorBoundary.reloadPage": "Reload Page",
"errorBoundary.title": "Something went wrong",
"errorBoundary.tryAgain": "Try Again",
"errorBoundary.tryAgainCompact": "Try again",
"errorMsg.fieldRequired": "{{field}} is required",
"errorMsg.urlError": "url should start with http:// or https://",
"feedback.content": "Feedback Content",