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();
},
});