diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.test.tsx b/packages/core-plugin-api/src/translation/useTranslationRef.test.tsx index 7b04f6935e..70d0c89a97 100644 --- a/packages/core-plugin-api/src/translation/useTranslationRef.test.tsx +++ b/packages/core-plugin-api/src/translation/useTranslationRef.test.tsx @@ -15,7 +15,11 @@ */ import React, { ReactNode } from 'react'; -import { TestApiProvider, withLogCollector } from '@backstage/test-utils'; +import { + MockErrorApi, + TestApiProvider, + withLogCollector, +} from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import { createTranslationRef } from './TranslationRef'; import { useTranslationRef } from './useTranslationRef'; @@ -28,6 +32,7 @@ import { TranslationApi, translationApiRef, } from '../alpha'; +import { ErrorApi, errorApiRef } from '../apis'; const plainRef = createTranslationRef({ id: 'plain', @@ -37,10 +42,16 @@ const plainRef = createTranslationRef({ }, }); -function makeWrapper(translationApi: TranslationApi) { +function makeWrapper( + translationApi: TranslationApi, + errorApi: ErrorApi = { error$: jest.fn(), post: jest.fn() }, +) { return ({ children }: { children: ReactNode }) => ( ); @@ -177,6 +188,7 @@ describe('useTranslationRef', () => { }, }); + const errorApi = new MockErrorApi({ collect: true }); const languageApi = AppLanguageSelector.create(); const translationApi = I18nextTranslationApi.create({ languageApi, @@ -193,21 +205,83 @@ describe('useTranslationRef', () => { }); const rendered1 = renderHook(() => useTranslationRef(ref), { - wrapper: makeWrapper(translationApi), + wrapper: makeWrapper(translationApi, errorApi), }); const rendered2 = renderHook(() => useTranslationRef(ref), { - wrapper: makeWrapper(translationApi), + wrapper: makeWrapper(translationApi, errorApi), }); const { error } = await withLogCollector(['error'], async () => { await rendered2.waitForNextUpdate(); }); - expect(error).toEqual([ - "Failed to load translation resource 'test'; caused by Error: NOPE", + const msg = + "Failed to load translation resource 'test'; caused by Error: NOPE"; + expect(error).toEqual([msg]); + expect(errorApi.getErrors()).toEqual([ + { + error: new Error(msg), + }, ]); expect(rendered1.result.current.t('key')).toBe('default'); expect(rendered2.result.current.t('key')).toBe('default'); }); + + it('should log once and then ignore loading errors after initial load', async () => { + const ref = createTranslationRef({ + id: 'test', + messages: { + key: 'default', + }, + }); + + const errorApi = new MockErrorApi({ collect: true }); + const languageApi = AppLanguageSelector.create({ + availableLanguages: ['en', 'de'], + }); + const translationApi = I18nextTranslationApi.create({ + languageApi, + resources: [ + createTranslationResource({ + ref, + translations: { + de: async () => { + throw new Error('NOPE'); + }, + }, + }), + ], + }); + + const { result } = renderHook(() => useTranslationRef(ref), { + wrapper: makeWrapper(translationApi, errorApi), + }); + + expect(result.current.t('key')).toBe('default'); + languageApi.setLanguage('de'); + + const { error } = await withLogCollector(['error'], async () => { + const rendered1 = renderHook(() => useTranslationRef(ref), { + wrapper: makeWrapper(translationApi, errorApi), + }); + const rendered2 = renderHook(() => useTranslationRef(ref), { + wrapper: makeWrapper(translationApi, errorApi), + }); + + await new Promise(resolve => setTimeout(resolve)); // Wait a long tick + + expect(rendered1.result.current.t('key')).toBe('default'); + expect(rendered2.result.current.t('key')).toBe('default'); + }); + + const msg = + "Failed to load translation resource 'test'; caused by Error: NOPE"; + expect(error).toEqual([msg]); + expect(errorApi.getErrors()).toEqual([ + { + error: new Error(msg), + }, + ]); + }); }); diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.ts b/packages/core-plugin-api/src/translation/useTranslationRef.ts index 4f09d0b970..50d6d4d85a 100644 --- a/packages/core-plugin-api/src/translation/useTranslationRef.ts +++ b/packages/core-plugin-api/src/translation/useTranslationRef.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { useEffect, useState } from 'react'; - -import { useApi } from '../apis'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { errorApiRef, useApi } from '../apis'; import { translationApiRef, TranslationFunction, @@ -33,14 +32,28 @@ export const useTranslationRef = < >( translationRef: TranslationRef, ): { t: TranslationFunction } => { + const errorApi = useApi(errorApiRef); const translationApi = useApi(translationApiRef); - const [error, setError] = useState(); const [snapshot, setSnapshot] = useState>(() => translationApi.getTranslation(translationRef), ); - const [observable] = useState(() => - translationApi.translation$(translationRef), + const observable = useMemo( + () => translationApi.translation$(translationRef), + [translationApi, translationRef], + ); + + const onError = useCallback( + (error: Error) => { + if (!loggedRefs.has(translationRef)) { + const errMsg = `Failed to load translation resource '${translationRef.id}'; caused by ${error}`; + // eslint-disable-next-line no-console + console.error(errMsg); + errorApi.post(new Error(errMsg)); + loggedRefs.add(translationRef); + } + }, + [errorApi, translationRef], ); useEffect(() => { @@ -50,17 +63,15 @@ export const useTranslationRef = < setSnapshot(next); } }, - error: setError, + error(error) { + onError(error); + }, }); return () => { subscription.unsubscribe(); }; - }, [observable]); - - if (error) { - throw error; - } + }, [observable, onError]); if (!snapshot.ready) { throw new Promise(resolve => { @@ -71,14 +82,9 @@ export const useTranslationRef = < resolve(); } }, - error(err) { - if (!loggedRefs.has(translationRef)) { - // eslint-disable-next-line no-console - console.error( - `Failed to load translation resource '${translationRef.id}'; caused by ${err}`, - ); - loggedRefs.add(translationRef); - } + error(error) { + subscription.unsubscribe(); + onError(error); resolve(); }, });