From b50c8e8237c5d200c3320d11922d171e55ce9d17 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 14 Sep 2023 19:54:23 +0200 Subject: [PATCH] core-plugin-api: refactor useTranslationRef to use new API Signed-off-by: Patrik Oldsberg --- .../src/translation/useTranslationRef.test.ts | 68 ------- .../translation/useTranslationRef.test.tsx | 178 ++++++++++++++++++ .../src/translation/useTranslationRef.ts | 65 +++++-- 3 files changed, 223 insertions(+), 88 deletions(-) delete mode 100644 packages/core-plugin-api/src/translation/useTranslationRef.test.ts create mode 100644 packages/core-plugin-api/src/translation/useTranslationRef.test.tsx 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 }; };