mirror of
https://github.com/langgenius/dify.git
synced 2026-04-15 01:38:19 +08:00
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:
parent
f27d669f87
commit
b54a0dc1e4
@ -1,6 +1,7 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createReactI18nextMock } from '@/test/i18n-mock'
|
||||||
import ErrorBoundary, { ErrorFallback, useAsyncError, useErrorHandler, withErrorBoundary } from '../index'
|
import ErrorBoundary, { ErrorFallback, useAsyncError, useErrorHandler, withErrorBoundary } from '../index'
|
||||||
|
|
||||||
const mockConfig = vi.hoisted(() => ({
|
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 = {
|
type ThrowOnRenderProps = {
|
||||||
message?: string
|
message?: string
|
||||||
shouldThrow: boolean
|
shouldThrow: boolean
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { ErrorInfo, ReactNode } from 'react'
|
|||||||
import { RiAlertLine, RiBugLine } from '@remixicon/react'
|
import { RiAlertLine, RiBugLine } from '@remixicon/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { IS_DEV } from '@/config'
|
import { IS_DEV } from '@/config'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
@ -29,9 +30,21 @@ type ErrorBoundaryProps = {
|
|||||||
customMessage?: string
|
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
|
// Internal class component for error catching
|
||||||
class ErrorBoundaryInner extends React.Component<
|
class ErrorBoundaryInner extends React.Component<
|
||||||
ErrorBoundaryProps & {
|
ErrorBoundaryProps & {
|
||||||
|
copy: ErrorBoundaryCopy
|
||||||
resetErrorBoundary: () => void
|
resetErrorBoundary: () => void
|
||||||
onResetKeysChange: (prevResetKeys?: Array<string | number>) => void
|
onResetKeysChange: (prevResetKeys?: Array<string | number>) => void
|
||||||
},
|
},
|
||||||
@ -96,6 +109,7 @@ class ErrorBoundaryInner extends React.Component<
|
|||||||
enableRecovery = true,
|
enableRecovery = true,
|
||||||
customTitle,
|
customTitle,
|
||||||
customMessage,
|
customMessage,
|
||||||
|
copy,
|
||||||
resetErrorBoundary,
|
resetErrorBoundary,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
@ -118,12 +132,12 @@ class ErrorBoundaryInner extends React.Component<
|
|||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<RiAlertLine className="text-state-critical-solid h-8 w-8" />
|
<RiAlertLine className="text-state-critical-solid h-8 w-8" />
|
||||||
<h2 className="text-xl font-semibold text-text-primary">
|
<h2 className="text-xl font-semibold text-text-primary">
|
||||||
{customTitle || 'Something went wrong'}
|
{customTitle || copy.title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mb-6 text-center text-text-secondary">
|
<p className="mb-6 text-center text-text-secondary">
|
||||||
{customMessage || 'An unexpected error occurred while rendering this component.'}
|
{customMessage || copy.message}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{showDetails && errorInfo && (
|
{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">
|
<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">
|
<span className="inline-flex items-center gap-1">
|
||||||
<RiBugLine className="h-4 w-4" />
|
<RiBugLine className="h-4 w-4" />
|
||||||
Error Details (Development Only)
|
{copy.details}
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="rounded-lg bg-gray-100 p-4">
|
<div className="rounded-lg bg-gray-100 p-4">
|
||||||
<div className="mb-2">
|
<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">
|
<pre className="mt-1 overflow-auto whitespace-pre-wrap font-mono text-xs text-gray-800">
|
||||||
{error.toString()}
|
{error.toString()}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
{errorInfo && (
|
{errorInfo && (
|
||||||
<div>
|
<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">
|
<pre className="mt-1 max-h-40 overflow-auto whitespace-pre-wrap font-mono text-xs text-gray-700">
|
||||||
{errorInfo.componentStack}
|
{errorInfo.componentStack}
|
||||||
</pre>
|
</pre>
|
||||||
@ -151,11 +165,7 @@ class ErrorBoundaryInner extends React.Component<
|
|||||||
)}
|
)}
|
||||||
{errorCount > 1 && (
|
{errorCount > 1 && (
|
||||||
<div className="mt-2 text-xs text-gray-600">
|
<div className="mt-2 text-xs text-gray-600">
|
||||||
This error has occurred
|
{copy.formatErrorCount(errorCount)}
|
||||||
{' '}
|
|
||||||
{errorCount}
|
|
||||||
{' '}
|
|
||||||
times
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -169,14 +179,14 @@ class ErrorBoundaryInner extends React.Component<
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={resetErrorBoundary}
|
onClick={resetErrorBoundary}
|
||||||
>
|
>
|
||||||
Try Again
|
{copy.tryAgain}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
>
|
>
|
||||||
Reload Page
|
{copy.reload}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -190,9 +200,20 @@ class ErrorBoundaryInner extends React.Component<
|
|||||||
|
|
||||||
// Main functional component wrapper
|
// Main functional component wrapper
|
||||||
const ErrorBoundary: React.FC<ErrorBoundaryProps> = (props) => {
|
const ErrorBoundary: React.FC<ErrorBoundaryProps> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [errorBoundaryKey, setErrorBoundaryKey] = useState(0)
|
const [errorBoundaryKey, setErrorBoundaryKey] = useState(0)
|
||||||
const resetKeysRef = useRef(props.resetKeys)
|
const resetKeysRef = useRef(props.resetKeys)
|
||||||
const prevResetKeysRef = useRef<Array<string | number> | undefined>(undefined)
|
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(() => {
|
const resetErrorBoundary = useCallback(() => {
|
||||||
setErrorBoundaryKey(prev => prev + 1)
|
setErrorBoundaryKey(prev => prev + 1)
|
||||||
@ -211,6 +232,7 @@ const ErrorBoundary: React.FC<ErrorBoundaryProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<ErrorBoundaryInner
|
<ErrorBoundaryInner
|
||||||
{...props}
|
{...props}
|
||||||
|
copy={copy}
|
||||||
key={errorBoundaryKey}
|
key={errorBoundaryKey}
|
||||||
resetErrorBoundary={resetErrorBoundary}
|
resetErrorBoundary={resetErrorBoundary}
|
||||||
onResetKeysChange={onResetKeysChange}
|
onResetKeysChange={onResetKeysChange}
|
||||||
@ -265,12 +287,14 @@ export const ErrorFallback: React.FC<{
|
|||||||
error: Error
|
error: Error
|
||||||
resetErrorBoundaryAction: () => void
|
resetErrorBoundaryAction: () => void
|
||||||
}> = ({ error, resetErrorBoundaryAction }) => {
|
}> = ({ error, resetErrorBoundaryAction }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[200px] flex-col items-center justify-center rounded-lg border border-red-200 bg-red-50 p-8">
|
<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>
|
<p className="mb-4 text-center text-red-600">{error.message}</p>
|
||||||
<Button onClick={resetErrorBoundaryAction} size="small">
|
<Button onClick={resetErrorBoundaryAction} size="small">
|
||||||
Try again
|
{t('errorBoundary.tryAgainCompact', { ns: 'common' })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,9 +3,17 @@ import type { TriggerSubscription } from '@/app/components/workflow/block-select
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||||
|
import { createReactI18nextMock } from '@/test/i18n-mock'
|
||||||
import { SubscriptionList } from '../index'
|
import { SubscriptionList } from '../index'
|
||||||
import { SubscriptionListMode } from '../types'
|
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()
|
const mockRefetch = vi.fn()
|
||||||
let mockSubscriptionListError: Error | null = null
|
let mockSubscriptionListError: Error | null = null
|
||||||
let mockSubscriptionListState: {
|
let mockSubscriptionListState: {
|
||||||
@ -209,12 +217,12 @@ describe('SubscriptionList', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Edge Cases', () => {
|
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')
|
mockSubscriptionListError = new Error('boom')
|
||||||
|
|
||||||
render(<SubscriptionList />)
|
render(<SubscriptionList />)
|
||||||
|
|
||||||
expect(screen.getByText('Something went wrong')).toBeInTheDocument()
|
expect(await screen.findByText('Something went wrong')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -162,6 +162,15 @@
|
|||||||
"environment.development": "DEVELOPMENT",
|
"environment.development": "DEVELOPMENT",
|
||||||
"environment.testing": "TESTING",
|
"environment.testing": "TESTING",
|
||||||
"error": "Error",
|
"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.fieldRequired": "{{field}} is required",
|
||||||
"errorMsg.urlError": "url should start with http:// or https://",
|
"errorMsg.urlError": "url should start with http:// or https://",
|
||||||
"feedback.content": "Feedback Content",
|
"feedback.content": "Feedback Content",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user