diff --git a/.changeset/breezy-houses-eat.md b/.changeset/breezy-houses-eat.md new file mode 100644 index 0000000000..184f7ecf45 --- /dev/null +++ b/.changeset/breezy-houses-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Add support for translation extensions. diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 472ed4905a..0f6df3e8b2 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -18,6 +18,7 @@ import { coreExtensionData, createExtension, createExtensionInput, + createTranslationExtension, } from '@backstage/frontend-plugin-api'; export const Core = createExtension({ @@ -33,6 +34,9 @@ export const Core = createExtension({ components: createExtensionInput({ component: coreExtensionData.component, }), + translations: createExtensionInput({ + translation: createTranslationExtension.translationDataRef, + }), root: createExtensionInput( { element: coreExtensionData.reactElement, diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index f58dfb3159..e8bb401504 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -23,6 +23,7 @@ import { ComponentRef, componentsApiRef, coreExtensionData, + createTranslationExtension, ExtensionDataRef, ExtensionOverrides, RouteRef, @@ -415,6 +416,16 @@ function createApiHolder( ?.map(e => e.instance?.getData(coreExtensionData.theme)) .filter((x): x is AppTheme => !!x) ?? []; + const translationResources = + tree.root.edges.attachments + .get('translations') + ?.map(e => + e.instance?.getData(createTranslationExtension.translationDataRef), + ) + .filter( + (x): x is typeof createTranslationExtension.translationDataRef.T => !!x, + ) ?? []; + for (const factory of [...defaultApis, ...pluginApis]) { factoryRegistry.register('default', factory); } @@ -471,15 +482,6 @@ function createApiHolder( factory: () => AppLanguageSelector.createWithStorage(), }); - factoryRegistry.register('default', { - api: translationApiRef, - deps: { languageApi: appLanguageApiRef }, - factory: ({ languageApi }) => - I18nextTranslationApi.create({ - languageApi, - }), - }); - factoryRegistry.register('static', { api: configApiRef, deps: {}, @@ -492,12 +494,13 @@ function createApiHolder( factory: () => AppLanguageSelector.createWithStorage(), }); - factoryRegistry.register('default', { + factoryRegistry.register('static', { api: translationApiRef, deps: { languageApi: appLanguageApiRef }, factory: ({ languageApi }) => I18nextTranslationApi.create({ languageApi, + resources: translationResources, }), }); diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index c491579eaf..c5b586615f 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -74,6 +74,8 @@ import { SignInPageProps } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { storageApiRef } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; +import { TranslationMessages } from '@backstage/core-plugin-api/alpha'; +import { TranslationResource } from '@backstage/core-plugin-api/alpha'; import { TypesToApiRefs } from '@backstage/core-plugin-api'; import { useApi } from '@backstage/core-plugin-api'; import { useApiHolder } from '@backstage/core-plugin-api'; @@ -628,6 +630,28 @@ export function createThemeExtension( theme: AppTheme, ): ExtensionDefinition; +// @public (undocumented) +export function createTranslationExtension(options: { + name?: string; + resource: TranslationResource | TranslationMessages; +}): ExtensionDefinition; + +// @public (undocumented) +export namespace createTranslationExtension { + const // (undocumented) + translationDataRef: ConfigurableExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + {} + >; +} + export { DiscoveryApi }; export { discoveryApiRef }; diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts new file mode 100644 index 0000000000..3a0eee91b5 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { + createTranslationRef, + createTranslationMessages, + createTranslationResource, +} from '@backstage/core-plugin-api/alpha'; +import { createTranslationExtension } from './createTranslationExtension'; + +const translationRef = createTranslationRef({ + id: 'test', + messages: { + a: 'a', + b: 'b', + }, +}); + +describe('createTranslationExtension', () => { + it('creates a translation message extension', () => { + const messages = createTranslationMessages({ + ref: translationRef, + messages: { + a: 'A', + }, + }); + const extension = createTranslationExtension({ resource: messages }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + kind: 'translation', + namespace: 'test', + attachTo: { id: 'core', input: 'translations' }, + disabled: false, + inputs: {}, + output: { + resource: createTranslationExtension.translationDataRef, + }, + factory: expect.any(Function), + }); + + expect(extension.factory({} as any)).toEqual({ + resource: messages, + }); + }); + + it('creates a translation resource extension', () => { + const resource = createTranslationResource({ + ref: translationRef, + translations: { + sv: () => + Promise.resolve({ + default: createTranslationMessages({ + ref: translationRef, + messages: { + a: 'Ä', + b: 'Ö', + }, + }), + }), + }, + }); + const extension = createTranslationExtension({ resource }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + kind: 'translation', + namespace: 'test', + attachTo: { id: 'core', input: 'translations' }, + disabled: false, + inputs: {}, + output: { + resource: createTranslationExtension.translationDataRef, + }, + factory: expect.any(Function), + }); + + expect(extension.factory({} as any)).toEqual({ resource }); + }); + + it('creates a translation resource extension with a name', () => { + expect( + createTranslationExtension({ + name: 'sv', + resource: createTranslationResource({ + ref: translationRef, + translations: { + sv: () => + Promise.resolve({ + default: createTranslationMessages({ + ref: translationRef, + messages: { + a: 'Ä', + b: 'Ö', + }, + }), + }), + }, + }), + }), + ).toEqual({ + $$type: '@backstage/ExtensionDefinition', + kind: 'translation', + namespace: 'test', + name: 'sv', + attachTo: { id: 'core', input: 'translations' }, + disabled: false, + inputs: {}, + output: { + resource: createTranslationExtension.translationDataRef, + }, + factory: expect.any(Function), + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts new file mode 100644 index 0000000000..b120a15c8d --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts @@ -0,0 +1,45 @@ +/* + * 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 { createExtension, createExtensionDataRef } from '../wiring'; +import { + TranslationResource, + TranslationMessages, +} from '@backstage/core-plugin-api/alpha'; + +/** @public */ +export function createTranslationExtension(options: { + name?: string; + resource: TranslationResource | TranslationMessages; +}) { + return createExtension({ + kind: 'translation', + namespace: options.resource.id, + name: options.name, + attachTo: { id: 'core', input: 'translations' }, + output: { + resource: createTranslationExtension.translationDataRef, + }, + factory: () => ({ resource: options.resource }), + }); +} + +/** @public */ +export namespace createTranslationExtension { + export const translationDataRef = createExtensionDataRef< + TranslationResource | TranslationMessages + >('core.translationResource'); +} diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index a3be81f2a3..80ad0290dd 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -20,3 +20,4 @@ export { createNavItemExtension } from './createNavItemExtension'; export { createSignInPageExtension } from './createSignInPageExtension'; export { createThemeExtension } from './createThemeExtension'; export { createComponentExtension } from './createComponentExtension'; +export { createTranslationExtension } from './createTranslationExtension';