diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.test.ts b/packages/core-plugin-api/src/translation/useTranslationRef.test.ts
deleted file mode 100644
index 54e4440928..0000000000
--- a/packages/core-plugin-api/src/translation/useTranslationRef.test.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2023 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { renderHook } from '@testing-library/react-hooks';
-import { useTranslation } from 'react-i18next';
-import { useApi } from '../apis';
-import { useTranslationRef } from './useTranslationRef';
-import { createTranslationRef } from './TranslationRef';
-
-jest.mock('../apis', () => ({
- ...jest.requireActual('../apis'),
- useApi: jest.fn(),
-}));
-
-jest.mock('react-i18next', () => ({
- useTranslation: jest.fn(),
-}));
-
-describe('useTranslationRef', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should return correct t', () => {
- const translationRef = createTranslationRef({
- id: 'ref-id',
- messages: {
- key1: 'default1',
- key2: 'default2',
- },
- });
-
- const tMock = jest.fn();
- tMock.mockReturnValue('translatedValue');
-
- const i18nMock = {
- language: 'en',
- t: tMock,
- };
-
- (useApi as jest.Mock).mockReturnValue({
- addResource: jest.fn(),
- });
-
- (useTranslation as jest.Mock).mockReturnValue(i18nMock);
-
- const { result } = renderHook(() => useTranslationRef(translationRef));
-
- const t = result.current;
-
- t('key1', { condition: 'v1' });
- expect(tMock).toHaveBeenCalledWith('key1', 'default1', { condition: 'v1' });
- t('key2');
- expect(tMock).toHaveBeenCalledWith('key2', 'default2', undefined);
- });
-});
diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.test.tsx b/packages/core-plugin-api/src/translation/useTranslationRef.test.tsx
new file mode 100644
index 0000000000..1d2682e2fb
--- /dev/null
+++ b/packages/core-plugin-api/src/translation/useTranslationRef.test.tsx
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { TestApiProvider } from '@backstage/test-utils';
+import { renderHook } from '@testing-library/react-hooks';
+import { createTranslationRef } from './TranslationRef';
+import { useTranslationRef } from './useTranslationRef';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { AppLanguageSelector } from '../../..//core-app-api/src/apis/implementations/AppLanguageApi';
+import { createTranslationResource, translationApiRef } from '../alpha';
+
+const plainRef = createTranslationRef({
+ id: 'plain',
+ messages: {
+ key1: 'default1',
+ key2: 'default2',
+ },
+});
+
+describe('useTranslationRef', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should show default translations', () => {
+ const languageApi = AppLanguageSelector.create();
+ const translationApi = I18nextTranslationApi.create({ languageApi });
+
+ const { result } = renderHook(() => useTranslationRef(plainRef), {
+ wrapper: ({ children }) => (
+
+ ),
+ });
+
+ const { t } = result.current;
+
+ expect(t('key1')).toBe('default1');
+ expect(t('key2')).toBe('default2');
+ });
+
+ it('should show load translation resource', async () => {
+ const languageApi = AppLanguageSelector.create();
+ const translationApi = I18nextTranslationApi.create({
+ languageApi,
+ resources: [
+ createTranslationResource({
+ ref: plainRef,
+ translations: {
+ en: () =>
+ Promise.resolve({ default: { key1: 'en1', key2: 'en2' } }),
+ },
+ }),
+ ],
+ });
+
+ const { result, waitForNextUpdate } = renderHook(
+ () => useTranslationRef(plainRef),
+ {
+ wrapper: ({ children }) => (
+
+ ),
+ },
+ );
+
+ await waitForNextUpdate();
+
+ const { t } = result.current;
+
+ expect(t('key1')).toBe('en1');
+ expect(t('key2')).toBe('en2');
+ });
+
+ it('should switch between languages', async () => {
+ const languageApi = AppLanguageSelector.create({
+ availableLanguages: ['en', 'de'],
+ });
+ const translationApi = I18nextTranslationApi.create({
+ languageApi,
+ resources: [
+ createTranslationResource({
+ ref: plainRef,
+ translations: {
+ de: () =>
+ Promise.resolve({ default: { key1: 'de1', key2: 'de2' } }),
+ },
+ }),
+ ],
+ });
+
+ const { result, waitForNextUpdate } = renderHook(
+ () => useTranslationRef(plainRef),
+ {
+ wrapper: ({ children }) => (
+
+ ),
+ },
+ );
+
+ const { t } = result.current;
+
+ expect(t('key1')).toBe('default1');
+ expect(t('key2')).toBe('default2');
+
+ languageApi.setLanguage('de');
+
+ await waitForNextUpdate();
+
+ const { t: t2 } = result.current;
+
+ expect(t2('key1')).toBe('de1');
+ expect(t2('key2')).toBe('de2');
+ });
+
+ it('should load default resource', async () => {
+ const resourceRef = createTranslationRef({
+ id: 'resource',
+ messages: {
+ key1: 'default1',
+ key2: 'default2',
+ },
+ translations: {
+ de: () => Promise.resolve({ default: { key1: 'de1', key2: 'de2' } }),
+ },
+ });
+
+ const languageApi = AppLanguageSelector.create({
+ defaultLanguage: 'de',
+ availableLanguages: ['en', 'de'],
+ });
+ const translationApi = I18nextTranslationApi.create({
+ languageApi,
+ });
+
+ const { result, waitForNextUpdate } = renderHook(
+ () => useTranslationRef(resourceRef),
+ {
+ wrapper: ({ children }) => (
+
+ ),
+ },
+ );
+
+ await waitForNextUpdate();
+
+ const { t } = result.current;
+
+ expect(t('key1')).toBe('de1');
+ expect(t('key2')).toBe('de2');
+ });
+});
diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.ts b/packages/core-plugin-api/src/translation/useTranslationRef.ts
index a11ab9c25b..9919ab3b38 100644
--- a/packages/core-plugin-api/src/translation/useTranslationRef.ts
+++ b/packages/core-plugin-api/src/translation/useTranslationRef.ts
@@ -14,36 +14,61 @@
* limitations under the License.
*/
-import { useTranslation } from 'react-i18next';
+import { useEffect, useState } from 'react';
import { useApi } from '../apis';
-import { appTranslationApiRef } from '../apis/alpha';
-import { toInternalTranslationRef, TranslationRef } from './TranslationRef';
-
-/** @alpha */
-export interface TranslationOptions {
- /* no options supported for now */
-}
+import {
+ translationApiRef,
+ TranslationFunction,
+ TranslationSnapshot,
+} from '../apis/alpha';
+import { TranslationRef } from './TranslationRef';
/** @alpha */
export const useTranslationRef = <
TMessages extends { [key in string]: string },
>(
translationRef: TranslationRef,
-) => {
- const translationApi = useApi(appTranslationApiRef);
+): { t: TranslationFunction } => {
+ const translationApi = useApi(translationApiRef);
- const internalRef = toInternalTranslationRef(translationRef);
- translationApi.addResource(translationRef);
+ const [error, setError] = useState();
+ const [snapshot, setSnapshot] = useState>(() =>
+ translationApi.getTranslation(translationRef),
+ );
+ const [observable] = useState(() =>
+ translationApi.translation$(translationRef),
+ );
- const { t } = useTranslation(internalRef.id, {
- useSuspense: process.env.NODE_ENV !== 'test',
- });
+ useEffect(() => {
+ const subscription = observable.subscribe({
+ next(next) {
+ if (next.ready) {
+ setSnapshot(next);
+ }
+ },
+ error: setError,
+ });
- const defaultMessages = internalRef.getDefaultMessages();
+ return () => {
+ subscription.unsubscribe();
+ };
+ }, [observable]);
- return (
- key: TKey,
- options?: TranslationOptions,
- ): TMessages[TKey] => t(key, defaultMessages[key], options);
+ if (error) {
+ throw error;
+ }
+
+ if (!snapshot.ready) {
+ throw new Promise(resolve => {
+ const subscription = observable.subscribe(next => {
+ if (next.ready) {
+ subscription.unsubscribe();
+ resolve();
+ }
+ });
+ });
+ }
+
+ return { t: snapshot.t };
};