From d3c7b71929568509162370b61124e1589c67bb46 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Sep 2023 12:30:29 +0200 Subject: [PATCH] core-app-api: initial translation api refactor for new message/resource strucutre Signed-off-by: Patrik Oldsberg --- ...ion.test.ts => AppTranslationImpl.test.ts} | 118 ++++++------------ .../AppTranslationApi/AppTranslationImpl.ts | 115 ++++++++--------- packages/core-app-api/src/app/types.ts | 34 ++--- .../src/apis/definitions/AppTranslationApi.ts | 4 +- .../src/translation/useTranslationRef.ts | 7 +- 5 files changed, 105 insertions(+), 173 deletions(-) rename packages/core-app-api/src/apis/implementations/AppTranslationApi/{AppTranslation.test.ts => AppTranslationImpl.test.ts} (55%) diff --git a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslation.test.ts b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.test.ts similarity index 55% rename from packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslation.test.ts rename to packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.test.ts index b6eba8a1c7..5b7304d36b 100644 --- a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslation.test.ts +++ b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.test.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { + createTranslationMessages, + createTranslationRef, + createTranslationResource, +} from '@backstage/core-plugin-api/alpha'; import { AppTranslationApiImpl } from './AppTranslationImpl'; import i18next from 'i18next'; @@ -70,112 +74,68 @@ describe('AppTranslationApiImpl', () => { }); it('should init messages correctly', () => { - const useResourcesMock = jest.spyOn( + const addMessagesMock = jest.spyOn( AppTranslationApiImpl.prototype, - 'addResources', + 'addMessages', ); - const useLazyResourcesMock = jest.spyOn( + const addLazyResourcesMock = jest.spyOn( AppTranslationApiImpl.prototype, 'addLazyResources', ); const ref = createTranslationRef({ id: 'ref-id', messages: { - key1: '', + key: '', }, }); - const options = { - supportedLanguages: ['en'], - messages: [ - { - ref, - messages: { - en: { key1: 'value1' }, - }, - lazyMessages: { - en: () => Promise.resolve({ key2: 'value2' }), - } as any, - }, - ], - }; - - const instance = AppTranslationApiImpl.create({ - supportedLanguages: ['en'], + const overrides = createTranslationMessages({ + ref, + messages: { key: 'value1' }, + }); + const resource = createTranslationResource({ + ref, + translations: { + en: () => + Promise.resolve({ + default: { + key: 'value2', + }, + }), + }, }); - instance.initMessages(options); - expect(useResourcesMock).toHaveBeenCalledWith( - options.messages[0].ref, - options.messages[0].messages, - ); - expect(useLazyResourcesMock).toHaveBeenCalledWith( - options.messages[0].ref, - options.messages[0].lazyMessages, - ); + AppTranslationApiImpl.create({ + supportedLanguages: ['en'], + resources: [overrides, resource], + }); + + expect(addMessagesMock).toHaveBeenCalledWith(overrides); + expect(addLazyResourcesMock).toHaveBeenCalledWith(resource); }); it('should useResources correctly', () => { - const i18nMock = i18next.createInstance() as any; - const useReturnMock = i18nMock.use(); - jest.spyOn(i18nMock, 'use').mockReturnValue(useReturnMock); - jest.spyOn(i18next, 'createInstance').mockReturnValue(i18nMock); - - const ref = createTranslationRef({ - id: 'ref-id', - messages: { - key1: 'value1', - }, - resources: { - en: { - key1: 'value2', - }, - }, - }); - - const instance = AppTranslationApiImpl.create({ - supportedLanguages: ['en'], - }); - instance.addResources(ref); - - expect(useReturnMock.addResourceBundle).toHaveBeenCalledWith( - 'en', - 'ref-id', - { key1: 'value2' }, - true, - false, + const addLazyResourcesMock = jest.spyOn( + AppTranslationApiImpl.prototype, + 'addLazyResources', ); - }); - - it('should useLazyResources correctly', () => { - const i18nMock = i18next.createInstance() as any; - const useReturnMock = i18nMock.use(); - - jest.spyOn(i18nMock, 'use').mockReturnValue(useReturnMock); - jest.spyOn(i18next, 'createInstance').mockReturnValue(i18nMock); const ref = createTranslationRef({ id: 'ref-id', messages: { key1: 'value1', }, - lazyResources: { - en: () => Promise.resolve({ messages: { key1: 'value2' } }), + translations: { + en: () => Promise.resolve({ default: { key1: 'value2' } }), }, }); const instance = AppTranslationApiImpl.create({ supportedLanguages: ['en'], }); - instance.addLazyResources(ref); - setTimeout(() => { - expect(useReturnMock.addResourceBundle).toHaveBeenCalledWith( - 'en', - 'ref-id', - { key1: 'value2' }, - true, - false, - ); - }); + instance.addResource(ref); + + expect(addLazyResourcesMock).toHaveBeenCalledTimes(1); + expect(addLazyResourcesMock.mock.calls[0][0].id).toBe('ref-id'); }); }); diff --git a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts index 519a406628..c55c8bd7e8 100644 --- a/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts +++ b/packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts @@ -16,31 +16,39 @@ import { AppTranslationApi, + TranslationMessages, TranslationRef, + TranslationResource, } from '@backstage/core-plugin-api/alpha'; import i18next, { type i18n } from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; -import { TranslationMessages } from '../../../alpha'; // Internal import to avoid code duplication, this will lead to duplication in build output // eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalTranslationResource } from '../../../../../core-plugin-api/src/translation/TranslationResource'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalTranslationRef } from '../../../../../core-plugin-api/src/translation/TranslationRef'; +const DEFAULT_LANGUAGE = 'en'; + /** @alpha */ export type ExperimentalI18n = { - supportedLanguages: string[]; fallbackLanguage?: string | string[]; - messages?: Array<{ - ref: TranslationRef; - messages?: Record>; - lazyMessages: Record< - string, - () => Promise<{ messages: TranslationMessages }> - >; - }>; + supportedLanguages?: string[]; + resources?: Array; }; +function removeNulls( + messages: Record, +): Record { + return Object.fromEntries( + Object.entries(messages).filter( + (e): e is [string, string] => e[1] !== null, + ), + ); +} + /** @alpha */ export class AppTranslationApiImpl implements AppTranslationApi { static create(options?: ExperimentalI18n) { @@ -49,8 +57,8 @@ export class AppTranslationApiImpl implements AppTranslationApi { i18n.use(LanguageDetector); i18n.init({ - fallbackLng: options?.fallbackLanguage || 'en', - supportedLngs: options?.supportedLanguages || ['en'], + fallbackLng: options?.fallbackLanguage || DEFAULT_LANGUAGE, + supportedLngs: options?.supportedLanguages || [DEFAULT_LANGUAGE], interpolation: { escapeValue: false, }, @@ -62,70 +70,52 @@ export class AppTranslationApiImpl implements AppTranslationApi { return new AppTranslationApiImpl(i18n, options); } - private readonly cache = new WeakSet(); - private readonly lazyCache = new WeakMap>(); + private readonly cache = new Set(); + private readonly lazyCache = new Map>(); getI18n() { return this.i18n; } initMessages(options?: ExperimentalI18n) { - if (options?.messages?.length) { - options.messages.forEach(appMessage => { - if (appMessage.messages) { - this.addResources(appMessage.ref, appMessage.messages); - } - - if (appMessage.lazyMessages) { - this.addLazyResources(appMessage.ref, appMessage.lazyMessages); - } - }); + for (const resource of options?.resources || []) { + if (resource.$$type === '@backstage/TranslationResource') { + this.addLazyResources(resource); + } else if (resource.$$type === '@backstage/TranslationMessages') { + // Overrides for default messages, created with createTranslationMessages and installed via app + this.addMessages(resource); + } } } - addResourcesByRef>( - translationRef: TranslationRef, - ): void { - this.addResources(translationRef); - this.addLazyResources(translationRef); + addResource(translationRef: TranslationRef): void { + const internalRef = toInternalTranslationRef(translationRef); + const defaultResource = internalRef.getDefaultResource(); + if (defaultResource) { + this.addLazyResources(defaultResource); + } } - addResources>( - translationRef: TranslationRef, - initResources?: Record< - string, - TranslationMessages> - >, - ) { - const internalRef = toInternalTranslationRef(translationRef); - const resources = initResources || internalRef.getResources(); - if (!resources || this.cache.has(internalRef)) { + addMessages(messages: TranslationMessages) { + if (this.cache.has(messages.id)) { return; } - this.cache.add(internalRef); - Object.entries(resources).forEach(([language, messages]) => { - this.i18n.addResourceBundle( - language, - internalRef.id, - messages, - true, - false, - ); - }); + this.cache.add(messages.id); + this.i18n.addResourceBundle( + DEFAULT_LANGUAGE, + messages.id, + removeNulls(messages.messages), + true, + false, + ); } - addLazyResources>( - translationRef: TranslationRef, - initResources?: Record< - string, - () => Promise<{ messages: TranslationMessages }> - >, - ) { - let cache = this.lazyCache.get(translationRef); + addLazyResources(resource: TranslationResource) { + let cache = this.lazyCache.get(resource.id); if (!cache) { cache = new Set(); - this.lazyCache.set(translationRef, cache); + this.lazyCache.set(resource.id, cache); } const { @@ -140,9 +130,8 @@ export class AppTranslationApiImpl implements AppTranslationApi { return; } - const internalRef = toInternalTranslationRef(translationRef); - const namespace = internalRef.id; - const lazyResources = initResources || internalRef.getLazyResources(); + const internalResource = toInternalTranslationResource(resource); + const namespace = internalResource.id; Promise.allSettled((options.supportedLngs || []).map(addLanguage)).then( results => { @@ -165,7 +154,9 @@ export class AppTranslationApiImpl implements AppTranslationApi { loadBackend = reloadResources([language], [namespace]); } - const loadLazyResources = lazyResources?.[language]; + const loadLazyResources = internalResource.resources.find( + entry => entry.language === language, + )?.loader; if (!loadLazyResources) { await loadBackend; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 3d4b9cf8c5..ea51723d21 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -27,7 +27,11 @@ import { FeatureFlag, } from '@backstage/core-plugin-api'; import { AppConfig } from '@backstage/config'; -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +import { + TranslationMessages, + TranslationRef, + TranslationResource, +} from '@backstage/core-plugin-api/alpha'; /** * Props for the `BootErrorPage` component of {@link AppComponents}. @@ -178,16 +182,6 @@ export type AppRouteBinder = < >, ) => void; -/** - * TODO: To be remove when TranslationMessages in packages/core-app-api/src/app/TranslationResource.ts - * come to be public - * - * @ignore - * */ -type TranslationMessages = T extends TranslationRef - ? Partial - : never; - /** * The options accepted by {@link createSpecializedApp}. * @@ -290,21 +284,11 @@ export type AppOptions = { */ bindRoutes?(context: { bind: AppRouteBinder }): void; - /** - * TODO: Change to ExperimentalI18n type when packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts - * become to public - */ - __experimentalI18n?: { - supportedLanguages: string[]; + // TODO: Change to ExperimentalI18n type when packages/core-app-api/src/apis/implementations/AppTranslationApi/AppTranslationImpl.ts + __experimentalTranslations?: { fallbackLanguage?: string | string[]; - messages?: Array<{ - ref: TranslationRef; - messages?: Record>; - lazyMessages: Record< - string, - () => Promise<{ messages: TranslationMessages }> - >; - }>; + supportedLanguages?: string[]; + resources?: Array; }; }; diff --git a/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts b/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts index b94e5add87..44f85c0c89 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AppTranslationApi.ts @@ -22,9 +22,7 @@ import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; export type AppTranslationApi = { getI18n(): i18n; - addResourcesByRef>( - translationRef: TranslationRef, - ): void; + addResource(resource: TranslationRef): void; }; /** diff --git a/packages/core-plugin-api/src/translation/useTranslationRef.ts b/packages/core-plugin-api/src/translation/useTranslationRef.ts index 605a220914..a11ab9c25b 100644 --- a/packages/core-plugin-api/src/translation/useTranslationRef.ts +++ b/packages/core-plugin-api/src/translation/useTranslationRef.ts @@ -29,13 +29,12 @@ export interface TranslationOptions { export const useTranslationRef = < TMessages extends { [key in string]: string }, >( - translationRef: TranslationRef, + translationRef: TranslationRef, ) => { - const appTranslationApi = useApi(appTranslationApiRef); - - appTranslationApi.addResourcesByRef(translationRef); + const translationApi = useApi(appTranslationApiRef); const internalRef = toInternalTranslationRef(translationRef); + translationApi.addResource(translationRef); const { t } = useTranslation(internalRef.id, { useSuspense: process.env.NODE_ENV !== 'test',