core-plugin-api: refactor useTranslationRef to use new API

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-14 19:54:23 +02:00
parent d93a3cdd1b
commit b50c8e8237
3 changed files with 223 additions and 88 deletions
@@ -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);
});
});
@@ -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 }) => (
<TestApiProvider
apis={[[translationApiRef, translationApi]]}
children={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 }) => (
<TestApiProvider
apis={[[translationApiRef, translationApi]]}
children={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 }) => (
<TestApiProvider
apis={[[translationApiRef, translationApi]]}
children={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 }) => (
<TestApiProvider
apis={[[translationApiRef, translationApi]]}
children={children}
/>
),
},
);
await waitForNextUpdate();
const { t } = result.current;
expect(t('key1')).toBe('de1');
expect(t('key2')).toBe('de2');
});
});
@@ -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<string, TMessages>,
) => {
const translationApi = useApi(appTranslationApiRef);
): { t: TranslationFunction<TMessages> } => {
const translationApi = useApi(translationApiRef);
const internalRef = toInternalTranslationRef(translationRef);
translationApi.addResource(translationRef);
const [error, setError] = useState<Error>();
const [snapshot, setSnapshot] = useState<TranslationSnapshot<TMessages>>(() =>
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 <TKey extends keyof TMessages & string>(
key: TKey,
options?: TranslationOptions,
): TMessages[TKey] => t(key, defaultMessages[key], options);
if (error) {
throw error;
}
if (!snapshot.ready) {
throw new Promise<void>(resolve => {
const subscription = observable.subscribe(next => {
if (next.ready) {
subscription.unsubscribe();
resolve();
}
});
});
}
return { t: snapshot.t };
};