mirror of
https://github.com/langgenius/dify.git
synced 2026-03-31 18:00:55 -04: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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user