From b54a0dc1e4acaa100ae7d8271d2db57614129e03 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:41:20 +0800 Subject: [PATCH] fix(web): localize error boundary copy (#34332) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../error-boundary/__tests__/index.spec.tsx | 14 +++++ .../components/base/error-boundary/index.tsx | 52 ++++++++++++++----- .../__tests__/index.spec.tsx | 12 ++++- web/i18n/en-US/common.json | 9 ++++ 4 files changed, 71 insertions(+), 16 deletions(-) diff --git a/web/app/components/base/error-boundary/__tests__/index.spec.tsx b/web/app/components/base/error-boundary/__tests__/index.spec.tsx index 8c34026175..b9838130f7 100644 --- a/web/app/components/base/error-boundary/__tests__/index.spec.tsx +++ b/web/app/components/base/error-boundary/__tests__/index.spec.tsx @@ -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 diff --git a/web/app/components/base/error-boundary/index.tsx b/web/app/components/base/error-boundary/index.tsx index 9cb4b70cf5..3e7bc03ed6 100644 --- a/web/app/components/base/error-boundary/index.tsx +++ b/web/app/components/base/error-boundary/index.tsx @@ -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) => 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<

- {customTitle || 'Something went wrong'} + {customTitle || copy.title}

- {customMessage || 'An unexpected error occurred while rendering this component.'} + {customMessage || copy.message}

{showDetails && errorInfo && ( @@ -131,19 +145,19 @@ class ErrorBoundaryInner extends React.Component< - Error Details (Development Only) + {copy.details}
- Error: + {copy.error}
                     {error.toString()}
                   
{errorInfo && (
- Component Stack: + {copy.componentStack}
                       {errorInfo.componentStack}
                     
@@ -151,11 +165,7 @@ class ErrorBoundaryInner extends React.Component< )} {errorCount > 1 && (
- This error has occurred - {' '} - {errorCount} - {' '} - times + {copy.formatErrorCount(errorCount)}
)}
@@ -169,14 +179,14 @@ class ErrorBoundaryInner extends React.Component< size="small" onClick={resetErrorBoundary} > - Try Again + {copy.tryAgain}
)} @@ -190,9 +200,20 @@ class ErrorBoundaryInner extends React.Component< // Main functional component wrapper const ErrorBoundary: React.FC = (props) => { + const { t } = useTranslation() const [errorBoundaryKey, setErrorBoundaryKey] = useState(0) const resetKeysRef = useRef(props.resetKeys) const prevResetKeysRef = useRef | 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 = (props) => { return ( void }> = ({ error, resetErrorBoundaryAction }) => { + const { t } = useTranslation() + return (
-

Oops! Something went wrong

+

{t('errorBoundary.fallbackTitle', { ns: 'common' })}

{error.message}

) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx index 5c7ebfc57a..d41dfaa7d0 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx @@ -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() - expect(screen.getByText('Something went wrong')).toBeInTheDocument() + expect(await screen.findByText('Something went wrong')).toBeInTheDocument() }) }) }) diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index 36301ed72b..c21aa25eae 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -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",