diff --git a/.changeset/friendly-oranges-greet.md b/.changeset/friendly-oranges-greet.md new file mode 100644 index 0000000000..d327c40640 --- /dev/null +++ b/.changeset/friendly-oranges-greet.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog': patch +--- + +Support i18n for catalog and catalog-react plugins diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index fb807d947d..b82c7ae3e2 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -13,6 +13,7 @@ import { PortableSchema } from '@backstage/frontend-plugin-api'; import { ResolvedExtensionInputs } from '@backstage/frontend-plugin-api'; import { ResourcePermission } from '@backstage/plugin-permission-common'; import { RouteRef } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) export const catalogExtensionData: { @@ -24,6 +25,64 @@ export const catalogExtensionData: { entityFilterExpression: ConfigurableExtensionDataRef; }; +// @alpha (undocumented) +export const catalogReactTranslationRef: TranslationRef< + 'catalog-react', + { + readonly 'catalogFilter.title': 'Filters'; + readonly 'catalogFilter.buttonTitle': 'Filters'; + readonly 'entityKindPicker.title': 'Kind'; + readonly 'entityKindPicker.errorMessage': 'Failed to load entity kinds'; + readonly 'entityLifecyclePicker.title': 'Lifecycle'; + readonly 'entityNamespacePicker.title': 'Namespace'; + readonly 'entityOwnerPicker.title': 'Owner'; + readonly 'entityProcessingStatusPicker.title': 'Processing Status'; + readonly 'entityTagPicker.title': 'Tags'; + readonly 'entityPeekAheadPopover.title': 'Drill into the entity to see all of the tags.'; + readonly 'entityPeekAheadPopover.entityCardActionsTitle': 'Show details'; + readonly 'entityPeekAheadPopover.emailCardAction.title': 'Email {{email}}'; + readonly 'entityPeekAheadPopover.emailCardAction.subTitle': 'mailto {{email}}'; + readonly 'entitySearchBar.placeholder': 'Search'; + readonly 'entityTypePicker.title': 'Type'; + readonly 'entityTypePicker.errorMessage': 'Failed to load entity types'; + readonly 'entityTypePicker.optionAllTitle': 'all'; + readonly 'favoriteEntity.addToFavorites': 'Add to favorites'; + readonly 'favoriteEntity.removeFromFavorites': 'Remove from favorites'; + readonly 'inspectEntityDialog.title': 'Entity Inspector'; + readonly 'inspectEntityDialog.closeButtonTitle': 'Close'; + readonly 'inspectEntityDialog.ancestryPage.title': 'Ancestry'; + readonly 'inspectEntityDialog.colocatedPage.title': 'Colocated'; + readonly 'inspectEntityDialog.colocatedPage.description': 'These are the entities that are colocated with this entity - as in, they originated from the same data source (e.g. came from the same YAML file), or from the same origin (e.g. the originally registered URL).'; + readonly 'inspectEntityDialog.colocatedPage.alertNoLocation': 'Entity had no location information.'; + readonly 'inspectEntityDialog.colocatedPage.alertNoEntity': 'There were no other entities on this location.'; + readonly 'inspectEntityDialog.jsonPage.title': 'Entity as JSON'; + readonly 'inspectEntityDialog.jsonPage.description': 'This is the raw entity data as received from the catalog, on JSON form.'; + readonly 'inspectEntityDialog.overviewPage.title': 'Overview'; + readonly 'inspectEntityDialog.yamlPage.title': 'Entity as YAML'; + readonly 'inspectEntityDialog.yamlPage.description': 'This is the raw entity data as received from the catalog, on YAML form.'; + readonly 'unregisterEntityDialog.title': 'Are you sure you want to unregister this entity?'; + readonly 'unregisterEntityDialog.cancelButtonTitle': 'Cancel'; + readonly 'unregisterEntityDialog.deleteButtonTitle': 'Delete Entity'; + readonly 'unregisterEntityDialog.deleteEntitySuccessMessage': 'Removed entity {{entityName}}'; + readonly 'unregisterEntityDialog.onlyDeleteStateTitle': 'This entity does not seem to originate from a registered location. You therefore only have the option to delete it outright from the catalog.'; + readonly 'unregisterEntityDialog.errorStateTitle': 'Internal error: Unknown state'; + readonly 'unregisterEntityDialog.bootstrapState.title': 'You cannot unregister this entity, since it originates from a protected Backstage configuration (location "{{location}}"). If you believe this is in error, please contact the {{appTitle}} integrator.'; + readonly 'unregisterEntityDialog.bootstrapState.advancedDescription': 'You have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.'; + readonly 'unregisterEntityDialog.bootstrapState.advancedOptions': 'Advanced Options'; + readonly 'unregisterEntityDialog.unregisterState.title': 'This action will unregister the following entities:'; + readonly 'unregisterEntityDialog.unregisterState.description': 'To undo, just re-register the entity in {{appTitle}}.'; + readonly 'unregisterEntityDialog.unregisterState.subTitle': 'Located at the following location:'; + readonly 'unregisterEntityDialog.unregisterState.advancedDescription': 'You also have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.'; + readonly 'unregisterEntityDialog.unregisterState.advancedOptions': 'Advanced Options'; + readonly 'unregisterEntityDialog.unregisterState.unregisterButtonTitle': 'Unregister Location'; + readonly 'userListPicker.defaultOrgName': 'Company'; + readonly 'userListPicker.orgFilterAllLabel': 'All'; + readonly 'userListPicker.personalFilter.title': 'Personal'; + readonly 'userListPicker.personalFilter.ownedLabel': 'Owned'; + readonly 'userListPicker.personalFilter.starredLabel': 'Starred'; + } +>; + // @alpha (undocumented) export function createEntityCardExtension< TConfig extends { diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index f5f4dfdb74..b1650d1f5b 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -32,6 +32,7 @@ import { Expand } from '../../../packages/frontend-plugin-api/src/types'; export { useEntityPermission } from './hooks/useEntityPermission'; export { isOwnerOf } from './utils'; +export * from './translation'; /** @alpha */ export const catalogExtensionData = { diff --git a/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx b/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx index 7583520412..ee51020cbb 100644 --- a/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx +++ b/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx @@ -23,6 +23,8 @@ import Typography from '@material-ui/core/Typography'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import { Theme, useTheme } from '@material-ui/core/styles'; import FilterListIcon from '@material-ui/icons/FilterList'; +import { catalogReactTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export const Filters = (props: { @@ -37,6 +39,7 @@ export const Filters = (props: { ); const theme = useTheme(); const [filterDrawerOpen, setFilterDrawerOpen] = useState(false); + const { t } = useTranslationRef(catalogReactTranslationRef); return isScreenSmallerThanBreakpoint ? ( <> @@ -45,7 +48,7 @@ export const Filters = (props: { onClick={() => setFilterDrawerOpen(true)} startIcon={} > - Filters + {t('catalogFilter.buttonTitle')} - Filters + {t('catalogFilter.title')} {props.children} diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx index 2513f8dca9..612c515d28 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx @@ -18,7 +18,7 @@ import { GetEntityFacetsResponse } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { ApiProvider } from '@backstage/core-app-api'; import { alertApiRef } from '@backstage/core-plugin-api'; -import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { fireEvent, waitFor, screen, within } from '@testing-library/react'; import { capitalize } from 'lodash'; import { default as React } from 'react'; @@ -75,7 +75,7 @@ describe('', () => { ); it('renders available entity kinds', async () => { - await renderWithEffects( + await renderInTestApp( ', () => { it('sets the selected kind filter', async () => { const updateFilters = jest.fn(); - await renderWithEffects( + await renderInTestApp( ', () => { it('respects the query parameter filter value', async () => { const updateFilters = jest.fn(); const queryParameters = { kind: 'group' }; - await renderWithEffects( + await renderInTestApp( ', () => { }); it('renders unknown kinds provided in query parameters', async () => { - await renderWithEffects( + await renderInTestApp( ', () => { }); it('limits kinds when allowedKinds is set', async () => { - await renderWithEffects( + await renderInTestApp( @@ -183,7 +183,7 @@ describe('', () => { }); it('renders kind from the query parameter even when not in allowedKinds', async () => { - await renderWithEffects( + await renderInTestApp( { const { allowedKinds, hidden, initialFilter = 'component' } = props; + const { t } = useTranslationRef(catalogReactTranslationRef); const alertApi = useApi(alertApiRef); @@ -106,11 +109,11 @@ export const EntityKindPicker = (props: EntityKindPickerProps) => { useEffect(() => { if (error) { alertApi.post({ - message: `Failed to load entity kinds`, + message: t('entityKindPicker.errorMessage'), severity: 'error', }); } - }, [error, alertApi]); + }, [error, alertApi, t]); if (error) return null; @@ -124,7 +127,7 @@ export const EntityKindPicker = (props: EntityKindPickerProps) => { return hidden ? null : ( 1 ? selectedTypes[0] : undefined) ?? 'all'} onChange={value => diff --git a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx index fd0a656317..8848857976 100644 --- a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx +++ b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx @@ -22,6 +22,8 @@ import Star from '@material-ui/icons/Star'; import StarBorder from '@material-ui/icons/StarBorder'; import React, { ComponentProps } from 'react'; import { useStarredEntity } from '../../hooks/useStarredEntity'; +import { catalogReactTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export type FavoriteEntityProps = ComponentProps & { @@ -43,6 +45,7 @@ export const FavoriteEntity = (props: FavoriteEntityProps) => { const { toggleStarredEntity, isStarredEntity } = useStarredEntity( props.entity, ); + const { t } = useTranslationRef(catalogReactTranslationRef); return ( { onClick={() => toggleStarredEntity()} > {isStarredEntity ? : } diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx index f155c3f164..644167acbb 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx @@ -30,6 +30,8 @@ import { ColocatedPage } from './components/ColocatedPage'; import { JsonPage } from './components/JsonPage'; import { OverviewPage } from './components/OverviewPage'; import { YamlPage } from './components/YamlPage'; +import { catalogReactTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles(theme => ({ fullHeightDialog: { @@ -95,6 +97,7 @@ export function InspectEntityDialog(props: { }) { const classes = useStyles(); const [activeTab, setActiveTab] = React.useState(0); + const { t } = useTranslationRef(catalogReactTranslationRef); useEffect(() => { setActiveTab(0); @@ -114,7 +117,7 @@ export function InspectEntityDialog(props: { PaperProps={{ className: classes.fullHeightDialog }} > - Entity Inspector + {t('inspectEntityDialog.title')}
@@ -152,7 +155,7 @@ export function InspectEntityDialog(props: { diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx index e08bac06e8..98bd27bd77 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx @@ -38,6 +38,8 @@ import { catalogApiRef } from '../../../api'; import { humanizeEntityRef } from '../../EntityRefLink'; import { entityRouteRef } from '../../../routes'; import { EntityKindIcon } from './EntityKindIcon'; +import { catalogReactTranslationRef } from '../../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles(theme => ({ node: { @@ -198,6 +200,7 @@ function CustomNode({ node }: DependencyGraphTypes.RenderNodeProps) { export function AncestryPage(props: { entity: Entity }) { const { loading, error, nodes, edges } = useAncestry(props.entity); + const { t } = useTranslationRef(catalogReactTranslationRef); if (loading) { return ; } else if (error) { @@ -206,7 +209,9 @@ export function AncestryPage(props: { entity: Entity }) { return ( <> - Ancestry + + {t('inspectEntityDialog.ancestryPage.title')} + This is the ancestry of entities above the current one - as in, the chain(s) of entities down to the current one, where{' '} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx index 97bf0f3cb8..6f89ff2c9f 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx @@ -32,6 +32,8 @@ import useAsync from 'react-use/esm/useAsync'; import { catalogApiRef } from '../../../api'; import { EntityRefLink } from '../../EntityRefLink'; import { KeyValueListItem, ListItemText } from './common'; +import { catalogReactTranslationRef } from '../../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles({ root: { @@ -95,6 +97,7 @@ function EntityList(props: { entities: Entity[]; header?: [string, string] }) { function Contents(props: { entity: Entity }) { const { entity } = props; + const { t } = useTranslationRef(catalogReactTranslationRef); const { loading, error, location, originLocation, colocatedEntities } = useColocated(entity); @@ -106,12 +109,14 @@ function Contents(props: { entity: Entity }) { if (!location && !originLocation) { return ( - Entity had no location information. + + {t('inspectEntityDialog.colocatedPage.alertNoLocation')} + ); } else if (!colocatedEntities?.length) { return ( - There were no other entities on this location. + {t('inspectEntityDialog.colocatedPage.alertNoEntity')} ); } @@ -148,13 +153,14 @@ function Contents(props: { entity: Entity }) { export function ColocatedPage(props: { entity: Entity }) { const classes = useStyles(); + const { t } = useTranslationRef(catalogReactTranslationRef); return ( <> - Colocated + + {t('inspectEntityDialog.colocatedPage.title')} + - These are the entities that are colocated with this entity - as in, they - originated from the same data source (e.g. came from the same YAML - file), or from the same origin (e.g. the originally registered URL). + {t('inspectEntityDialog.colocatedPage.description')}
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx index b6bda65229..af8a724729 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx @@ -19,13 +19,18 @@ import { CodeSnippet } from '@backstage/core-components'; import DialogContentText from '@material-ui/core/DialogContentText'; import React from 'react'; import { sortKeys } from './util'; +import { catalogReactTranslationRef } from '../../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export function JsonPage(props: { entity: Entity }) { + const { t } = useTranslationRef(catalogReactTranslationRef); return ( <> - Entity as JSON + + {t('inspectEntityDialog.jsonPage.title')} + - This is the raw entity data as received from the catalog, on JSON form. + {t('inspectEntityDialog.jsonPage.description')}
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx index ad742459de..361910b2cd 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx @@ -36,6 +36,8 @@ import { } from './common'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { CopyTextButton } from '@backstage/core-components'; +import { catalogReactTranslationRef } from '../../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles({ root: { @@ -59,11 +61,14 @@ export function OverviewPage(props: { entity: AlphaEntity }) { sortBy(relations, r => r.targetRef), 'type', ); + const { t } = useTranslationRef(catalogReactTranslationRef); const entityRef = stringifyEntityRef(props.entity); return ( <> - Overview + + {t('inspectEntityDialog.overviewPage.title')} +
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx index ad74611ec5..4e56b9b42a 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx @@ -20,13 +20,18 @@ import DialogContentText from '@material-ui/core/DialogContentText'; import React from 'react'; import YAML from 'yaml'; import { sortKeys } from './util'; +import { catalogReactTranslationRef } from '../../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export function YamlPage(props: { entity: Entity }) { + const { t } = useTranslationRef(catalogReactTranslationRef); return ( <> - Entity as YAML + + {t('inspectEntityDialog.yamlPage.title')} + - This is the raw entity data as received from the catalog, on YAML form. + {t('inspectEntityDialog.yamlPage.description')}
diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index 7f41badca5..e2ed9387e1 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -32,6 +32,8 @@ import { useUnregisterEntityDialogState } from './useUnregisterEntityDialogState import { alertApiRef, configApiRef, useApi } from '@backstage/core-plugin-api'; import { Progress, ResponseErrorPanel } from '@backstage/core-components'; import { assertError } from '@backstage/errors'; +import { catalogReactTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles({ advancedButton: { @@ -58,6 +60,7 @@ const Contents = ({ const [showDelete, setShowDelete] = useState(false); const [busy, setBusy] = useState(false); const appTitle = configApi.getOptionalString('app.title') ?? 'Backstage'; + const { t } = useTranslationRef(catalogReactTranslationRef); const onUnregister = useCallback( async function onUnregisterFn() { @@ -86,7 +89,9 @@ const Contents = ({ const entityName = entity.metadata.title ?? entity.metadata.name; onConfirm(); alertApi.post({ - message: `Removed entity ${entityName}`, + message: t('unregisterEntityDialog.deleteEntitySuccessMessage', { + entityName, + }), severity: 'success', display: 'transient', }); @@ -98,13 +103,13 @@ const Contents = ({ } } }, - [alertApi, onConfirm, state, entity], + [alertApi, onConfirm, state, entity, t], ); const DialogActionsPanel = () => ( ); @@ -121,10 +126,10 @@ const Contents = ({ return ( <> - You cannot unregister this entity, since it originates from a - protected Backstage configuration (location "{state.location}"). If - you believe this is in error, please contact the {appTitle}{' '} - integrator. + {t('unregisterEntityDialog.bootstrapState.title', { + appTitle, + location: state.location, + })} @@ -137,7 +142,7 @@ const Contents = ({ className={classes.advancedButton} onClick={() => setShowDelete(true)} > - Advanced Options + {t('unregisterEntityDialog.bootstrapState.advancedOptions')} @@ -146,11 +151,7 @@ const Contents = ({ {showDelete && ( <> - You have the option to delete the entity itself from the - catalog. Note that this should only be done if you know that the - catalog file has been deleted at, or moved from, its origin - location. If that is not the case, the entity will reappear - shortly as the next refresh round is performed by the catalog. + {t('unregisterEntityDialog.bootstrapState.advancedDescription')} @@ -172,8 +173,7 @@ const Contents = ({ return ( <> - This entity does not seem to originate from a registered location. You - therefore only have the option to delete it outright from the catalog. + {t('unregisterEntityDialog.onlyDeleteStateTitle')} @@ -192,7 +192,7 @@ const Contents = ({ return ( <> - This action will unregister the following entities: + {t('unregisterEntityDialog.unregisterState.title')} {state.colocatedEntities.map(e => ( @@ -202,13 +202,15 @@ const Contents = ({ ))} - Located at the following location: + {t('unregisterEntityDialog.unregisterState.subTitle')}
  • {state.location}
  • - To undo, just re-register the entity in {appTitle}. + {t('unregisterEntityDialog.unregisterState.description', { + appTitle, + })} {!showDelete && ( @@ -228,7 +230,7 @@ const Contents = ({ className={classes.advancedButton} onClick={() => setShowDelete(true)} > - Advanced Options + {t('unregisterEntityDialog.unregisterState.advancedOptions')} )} @@ -240,11 +242,7 @@ const Contents = ({ - You also have the option to delete the entity itself from the - catalog. Note that this should only be done if you know that the - catalog file has been deleted at, or moved from, its origin - location. If that is not the case, the entity will reappear - shortly as the next refresh round is performed by the catalog. + {t('unregisterEntityDialog.unregisterState.advancedDescription')} )} @@ -260,7 +258,11 @@ const Contents = ({ ); } - return Internal error: Unknown state; + return ( + + {t('unregisterEntityDialog.errorStateTitle')} + + ); }; /** @public */ @@ -274,10 +276,11 @@ export type UnregisterEntityDialogProps = { /** @public */ export const UnregisterEntityDialog = (props: UnregisterEntityDialogProps) => { const { open, onConfirm, onClose, entity } = props; + const { t } = useTranslationRef(catalogReactTranslationRef); return ( - Are you sure you want to unregister this entity? + {t('unregisterEntityDialog.title')} diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 5667447503..0b101e8174 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { fireEvent, render, waitFor, screen } from '@testing-library/react'; +import { fireEvent, waitFor, screen } from '@testing-library/react'; import { UserEntity } from '@backstage/catalog-model'; import { UserListPicker, UserListPickerProps } from './UserListPicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; @@ -30,7 +30,11 @@ import { QueryEntitiesInitialRequest, } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; -import { MockStorageApi, TestApiRegistry } from '@backstage/test-utils'; +import { + MockStorageApi, + TestApiRegistry, + renderInTestApp, +} from '@backstage/test-utils'; import { ApiProvider } from '@backstage/core-app-api'; import { ConfigApi, @@ -145,7 +149,7 @@ describe('', () => { }); it('renders filter groups', async () => { - render( + await renderInTestApp( @@ -164,7 +168,7 @@ describe('', () => { }); it('renders filters', async () => { - render( + await renderInTestApp( ', () => { }); it('respects other frontend filters in counts', async () => { - render( + await renderInTestApp( ', () => { it('respects the query parameter filter value', async () => { const updateFilters = jest.fn(); const queryParameters = { user: 'owned', kind: 'component' }; - render( + await renderInTestApp( ', () => { it('updates user filter when a menuitem is selected', async () => { const updateFilters = jest.fn(); - render( + await renderInTestApp( ', () => { it('responds to external queryParameters changes', async () => { const updateFilters = jest.fn(); - const rendered = render( + const rendered = await renderInTestApp( ', () => { it('does not reset the filter while entities are loading', async () => { mockCatalogApi.queryEntities?.mockReturnValue(new Promise(() => {})); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled(), @@ -433,7 +437,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -444,7 +448,7 @@ describe('', () => { }); it('does not reset the filter when request is empty', async () => { - render(); + await renderInTestApp(); await waitFor(() => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(1); @@ -473,7 +477,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ @@ -489,7 +493,7 @@ describe('', () => { () => new Promise(() => {}), ); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled(), @@ -512,7 +516,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -537,7 +541,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ @@ -563,7 +567,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -574,7 +578,7 @@ describe('', () => { }); it('does not reset the filter when entities are loaded', async () => { - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -588,7 +592,7 @@ describe('', () => { }); it('does not reset the filter when request is empty xxxx', async () => { - render(); + await renderInTestApp(); await waitFor(() => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(1); @@ -619,7 +623,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -630,7 +634,7 @@ describe('', () => { }); it('does not reset the filter when entities are loaded', async () => { - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index 1e46258885..7f0ff8be74 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -36,6 +36,11 @@ import { UserListFilterKind } from '../../types'; import { useOwnedEntitiesCount } from './useOwnedEntitiesCount'; import { useAllEntitiesCount } from './useAllEntitiesCount'; import { useStarredEntitiesCount } from './useStarredEntitiesCount'; +import { + TranslationFunction, + useTranslationRef, +} from '@backstage/core-plugin-api/alpha'; +import { catalogReactTranslationRef } from '../../translation'; /** @public */ export type CatalogReactUserListPickerClassKey = @@ -83,29 +88,32 @@ export type ButtonGroup = { }[]; }; -function getFilterGroups(orgName: string | undefined): ButtonGroup[] { +function getFilterGroups( + orgName: string, + t: TranslationFunction, +): ButtonGroup[] { return [ { - name: 'Personal', + name: t('userListPicker.personalFilter.title'), items: [ { id: 'owned', - label: 'Owned', + label: t('userListPicker.personalFilter.ownedLabel'), icon: SettingsIcon, }, { id: 'starred', - label: 'Starred', + label: t('userListPicker.personalFilter.starredLabel'), icon: StarIcon, }, ], }, { - name: orgName ?? 'Company', + name: orgName, items: [ { id: 'all', - label: 'All', + label: t('userListPicker.orgFilterAllLabel'), }, ], }, @@ -123,7 +131,10 @@ export const UserListPicker = (props: UserListPickerProps) => { const { initialFilter, availableFilters } = props; const classes = useStyles(); const configApi = useApi(configApiRef); - const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; + const { t } = useTranslationRef(catalogReactTranslationRef); + const orgName = + configApi.getOptionalString('organization.name') ?? + t('userListPicker.defaultOrgName'); const { filters, updateFilters, @@ -133,7 +144,7 @@ export const UserListPicker = (props: UserListPickerProps) => { // Remove group items that aren't in availableFilters and exclude // any now-empty groups. const userAndGroupFilterIds = ['starred', 'all']; - const filterGroups = getFilterGroups(orgName) + const filterGroups = getFilterGroups(orgName, t) .map(filterGroup => ({ ...filterGroup, items: filterGroup.items.filter(({ id }) => diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index bc8e6b6038..9afa81e2dd 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -20,6 +20,7 @@ import { alertApiRef, ConfigApi, configApiRef, + errorApiRef, IdentityApi, identityApiRef, storageApiRef, @@ -39,6 +40,8 @@ import { } from '../filters'; import { EntityListProvider, useEntityList } from './useEntityListProvider'; import { useMountEffect } from '@react-hookz/web'; +import { translationApiRef } from '@backstage/core-plugin-api/alpha'; +import { MockTranslationApi } from '@backstage/test-utils/alpha'; const entities: Entity[] = [ { @@ -110,6 +113,8 @@ const createWrapper = [storageApiRef, MockStorageApi.create()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], [alertApiRef, { post: jest.fn() }], + [translationApiRef, MockTranslationApi.create()], + [errorApiRef, { error$: jest.fn(), post: jest.fn() }], ]} > diff --git a/plugins/catalog-react/src/translation.ts b/plugins/catalog-react/src/translation.ts new file mode 100644 index 0000000000..7f9661380f --- /dev/null +++ b/plugins/catalog-react/src/translation.ts @@ -0,0 +1,128 @@ +/* + * Copyright 2024 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 } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const catalogReactTranslationRef = createTranslationRef({ + id: 'catalog-react', + messages: { + catalogFilter: { + title: 'Filters', + buttonTitle: 'Filters', + }, + entityKindPicker: { + title: 'Kind', + errorMessage: 'Failed to load entity kinds', + }, + entityLifecyclePicker: { + title: 'Lifecycle', + }, + entityNamespacePicker: { + title: 'Namespace', + }, + entityOwnerPicker: { + title: 'Owner', + }, + entityProcessingStatusPicker: { + title: 'Processing Status', + }, + entityTagPicker: { + title: 'Tags', + }, + entityPeekAheadPopover: { + title: 'Drill into the entity to see all of the tags.', + emailCardAction: { + title: 'Email {{email}}', + subTitle: 'mailto {{email}}', + }, + entityCardActionsTitle: 'Show details', + }, + entitySearchBar: { + placeholder: 'Search', + }, + entityTypePicker: { + title: 'Type', + errorMessage: 'Failed to load entity types', + optionAllTitle: 'all', + }, + favoriteEntity: { + addToFavorites: 'Add to favorites', + removeFromFavorites: 'Remove from favorites', + }, + inspectEntityDialog: { + title: 'Entity Inspector', + closeButtonTitle: 'Close', + ancestryPage: { + title: 'Ancestry', + }, + colocatedPage: { + title: 'Colocated', + description: + 'These are the entities that are colocated with this entity - as in, they originated from the same data source (e.g. came from the same YAML file), or from the same origin (e.g. the originally registered URL).', + alertNoLocation: 'Entity had no location information.', + alertNoEntity: 'There were no other entities on this location.', + }, + jsonPage: { + title: 'Entity as JSON', + description: + 'This is the raw entity data as received from the catalog, on JSON form.', + }, + overviewPage: { + title: 'Overview', + }, + yamlPage: { + title: 'Entity as YAML', + description: + 'This is the raw entity data as received from the catalog, on YAML form.', + }, + }, + unregisterEntityDialog: { + title: 'Are you sure you want to unregister this entity?', + cancelButtonTitle: 'Cancel', + deleteButtonTitle: 'Delete Entity', + deleteEntitySuccessMessage: 'Removed entity {{entityName}}', + bootstrapState: { + title: + 'You cannot unregister this entity, since it originates from a protected Backstage configuration (location "{{location}}"). If you believe this is in error, please contact the {{appTitle}} integrator.', + advancedDescription: + 'You have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.', + advancedOptions: 'Advanced Options', + }, + onlyDeleteStateTitle: + 'This entity does not seem to originate from a registered location. You therefore only have the option to delete it outright from the catalog.', + unregisterState: { + title: 'This action will unregister the following entities:', + subTitle: 'Located at the following location:', + description: 'To undo, just re-register the entity in {{appTitle}}.', + unregisterButtonTitle: 'Unregister Location', + advancedOptions: 'Advanced Options', + advancedDescription: + 'You also have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.', + }, + errorStateTitle: 'Internal error: Unknown state', + }, + userListPicker: { + defaultOrgName: 'Company', + personalFilter: { + title: 'Personal', + ownedLabel: 'Owned', + starredLabel: 'Starred', + }, + orgFilterAllLabel: 'All', + }, + }, +}); diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md index 75d7e53873..102e2a7aa3 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -17,8 +17,82 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha'; export const catalogTranslationRef: TranslationRef< 'catalog', { + readonly 'deleteEntity.description': 'This entity is not referenced by any location and is therefore not receiving updates. Click here to delete.'; + readonly 'deleteEntity.cancelButtonTitle': 'Cancel'; + readonly 'deleteEntity.deleteButtonTitle': 'Delete'; + readonly 'deleteEntity.dialogTitle': 'Are you sure you want to delete this entity?'; readonly 'indexPage.title': '{{orgName}} Catalog'; readonly 'indexPage.createButtonTitle': 'Create'; + readonly 'indexPage.supportButtonContent': 'All your software catalog entities'; + readonly 'aboutCard.title': 'About'; + readonly 'aboutCard.refreshButtonTitle': 'Schedule entity refresh'; + readonly 'aboutCard.editButtonTitle': 'Edit Metadata'; + readonly 'aboutCard.createSimilarButtonTitle': 'Create something similar'; + readonly 'aboutCard.refreshScheduledMessage': 'Refresh scheduled'; + readonly 'aboutCard.launchTemplate': 'Launch Template'; + readonly 'aboutCard.viewTechdocs': 'View TechDocs'; + readonly 'aboutCard.viewSource': 'View Source'; + readonly 'aboutCard.descriptionField.value': 'No description'; + readonly 'aboutCard.descriptionField.label': 'Description'; + readonly 'aboutCard.ownerField.value': 'No Owner'; + readonly 'aboutCard.ownerField.label': 'Owner'; + readonly 'aboutCard.domainField.value': 'No Domain'; + readonly 'aboutCard.domainField.label': 'Domain'; + readonly 'aboutCard.systemField.value': 'No System'; + readonly 'aboutCard.systemField.label': 'System'; + readonly 'aboutCard.parentComponentField.value': 'No Parent Component'; + readonly 'aboutCard.parentComponentField.label': 'Parent Component'; + readonly 'aboutCard.typeField.label': 'Type'; + readonly 'aboutCard.lifecycleField.label': 'Lifecycle'; + readonly 'aboutCard.tagsField.value': 'No Tags'; + readonly 'aboutCard.tagsField.label': 'Tags'; + readonly 'aboutCard.targetsField.label': 'Targets'; + readonly 'searchResultItem.lifecycle': 'Lifecycle'; + readonly 'searchResultItem.Owner': 'Owner'; + readonly 'catalogTable.warningPanelTitle': 'Could not fetch catalog entities.'; + readonly 'catalogTable.viewActionTitle': 'View'; + readonly 'catalogTable.editActionTitle': 'Edit'; + readonly 'catalogTable.starActionTitle': 'Add to favorites'; + readonly 'catalogTable.unStarActionTitle': 'Remove from favorites'; + readonly 'dependencyOfComponentsCard.title': 'Dependency of components'; + readonly 'dependencyOfComponentsCard.emptyMessage': 'No component depends on this component'; + readonly 'dependsOnComponentsCard.title': 'Depends on components'; + readonly 'dependsOnComponentsCard.emptyMessage': 'No component is a dependency of this component'; + readonly 'dependsOnResourcesCard.title': 'Depends on resources'; + readonly 'dependsOnResourcesCard.emptyMessage': 'No resource is a dependency of this component'; + readonly 'entityContextMenu.copiedMessage': 'Copied!'; + readonly 'entityContextMenu.moreButtonTitle': 'More'; + readonly 'entityContextMenu.inspectMenuTitle': 'Inspect entity'; + readonly 'entityContextMenu.copyURLMenuTitle': 'Copy entity URL'; + readonly 'entityContextMenu.unregisterMenuTitle': 'Unregister entity'; + readonly 'entityLabelsCard.title': 'Labels'; + readonly 'entityLabelsCard.emptyDescription': 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:'; + readonly 'entityLabelsCard.readMoreButtonTitle': 'Read more'; + readonly 'entityLabels.warningPanelTitle': 'Entity not found'; + readonly 'entityLabels.ownerLabel': 'Owner'; + readonly 'entityLabels.lifecycleLabel': 'Lifecycle'; + readonly 'entityLinksCard.title': 'Links'; + readonly 'entityLinksCard.emptyDescription': 'No links defined for this entity. You can add links to your entity YAML as shown in the highlighted example below:'; + readonly 'entityLinksCard.readMoreButtonTitle': 'Read more'; + readonly 'entityNotFound.title': 'Entity was not found'; + readonly 'entityNotFound.description': 'Want to help us build this? Check out our Getting Started documentation.'; + readonly 'entityNotFound.docButtonTitle': 'DOCS'; + readonly entityProcessingErrorsDescription: 'The error below originates from'; + readonly entityRelationWarningDescription: "This entity has relations to other entities, which can't be found in the catalog.\n Entities not found are: "; + readonly 'hasComponentsCard.title': 'Has components'; + readonly 'hasComponentsCard.emptyMessage': 'No component is part of this system'; + readonly 'hasResourcesCard.title': 'Has resources'; + readonly 'hasResourcesCard.emptyMessage': 'No resource is part of this system'; + readonly 'hasSubcomponentsCard.title': 'Has subcomponents'; + readonly 'hasSubcomponentsCard.emptyMessage': 'No subcomponent is part of this component'; + readonly 'hasSystemsCard.title': 'Has systems'; + readonly 'hasSystemsCard.emptyMessage': 'No system is part of this domain'; + readonly 'relatedEntitiesCard.emptyHelpLinkTitle': 'Learn how to change this.'; + readonly 'systemDiagramCard.title': 'System Diagram'; + readonly 'systemDiagramCard.description': 'Use pinch & zoo to move around the diagram.'; + readonly 'systemDiagramCard.edgeLabels.dependsOn': 'depends on'; + readonly 'systemDiagramCard.edgeLabels.partOf': 'part of'; + readonly 'systemDiagramCard.edgeLabels.provides': 'provides'; } >; diff --git a/plugins/catalog/src/alpha.ts b/plugins/catalog/src/alpha.ts index 927d5362b4..b78a5e6225 100644 --- a/plugins/catalog/src/alpha.ts +++ b/plugins/catalog/src/alpha.ts @@ -17,3 +17,4 @@ export * from './alpha/index'; export { default } from './alpha/index'; export { catalogTranslationRef } from './translation'; +export * from './translation'; diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index ce975f28ad..ff5d7fc801 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -64,6 +64,8 @@ import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common import { useSourceTemplateCompoundEntityRef } from './hooks'; import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha'; import { usePermission } from '@backstage/plugin-permission-react'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref'; @@ -120,6 +122,7 @@ export function AboutCard(props: AboutCardProps) { const { allowed: canRefresh } = useEntityPermission( catalogEntityRefreshPermission, ); + const { t } = useTranslationRef(catalogTranslationRef); const { allowed: canCreateTemplateTask } = usePermission({ permission: taskCreatePermission, @@ -146,13 +149,13 @@ export function AboutCard(props: AboutCardProps) { } const viewInSource: IconLinkVerticalProps = { - label: 'View Source', + label: t('aboutCard.viewSource'), disabled: !entitySourceLocation, icon: , href: entitySourceLocation?.locationTargetUrl, }; const viewInTechDocs: IconLinkVerticalProps = { - label: 'View TechDocs', + label: t('aboutCard.viewTechdocs'), disabled: !( entity.metadata.annotations?.[TECHDOCS_ANNOTATION] || @@ -180,7 +183,7 @@ export function AboutCard(props: AboutCardProps) { const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon; const launchTemplate: IconLinkVerticalProps = { - label: 'Launch Template', + label: t('aboutCard.launchTemplate'), icon: , disabled: !templateRoute || !canCreateTemplateTask, href: @@ -216,25 +219,25 @@ export function AboutCard(props: AboutCardProps) { try { await catalogApi.refreshEntity(stringifyEntityRef(entity)); alertApi.post({ - message: 'Refresh scheduled', + message: t('aboutCard.refreshScheduledMessage'), severity: 'info', display: 'transient', }); } catch (e) { errorApi.post(e); } - }, [catalogApi, alertApi, errorApi, entity]); + }, [catalogApi, entity, alertApi, t, errorApi]); return ( {allowRefresh && canRefresh && ( @@ -244,7 +247,7 @@ export function AboutCard(props: AboutCardProps) { component={Link} aria-label="Edit" disabled={!entityMetadataEditUrl} - title="Edit Metadata" + title={t('aboutCard.editButtonTitle')} to={entityMetadataEditUrl ?? '#'} > @@ -252,7 +255,7 @@ export function AboutCard(props: AboutCardProps) { {sourceTemplateRef && templateRoute && ( - + {ownedByRelations.length > 0 && ( @@ -130,8 +140,8 @@ export function AboutContent(props: AboutContentProps) { {(isSystem || partOfDomainRelations.length > 0) && ( {partOfDomainRelations.length > 0 && ( @@ -147,8 +157,8 @@ export function AboutContent(props: AboutContentProps) { isResource || partOfSystemRelations.length > 0) && ( {partOfSystemRelations.length > 0 && ( @@ -161,8 +171,8 @@ export function AboutContent(props: AboutContentProps) { )} {isComponent && partOfComponentRelations.length > 0 && ( @@ -188,22 +198,25 @@ export function AboutContent(props: AboutContentProps) { isComponent || typeof entity?.spec?.lifecycle === 'string') && ( )} - {(entity?.metadata?.tags || []).map(t => ( - + {(entity?.metadata?.tags || []).map(tag => ( + ))} {isLocation && (entity?.spec?.targets || entity?.spec?.target) && ( - + )} - All your software catalog entities + {t('indexPage.supportButtonContent')} diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index 32c86f38b7..1a596f8b9b 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -27,6 +27,8 @@ import { ResultHighlight, } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; const useStyles = makeStyles( { @@ -66,6 +68,7 @@ export function CatalogSearchResultListItem( const highlight = props.highlight as ResultHighlight; const classes = useStyles(); + const { t } = useTranslationRef(catalogTranslationRef); if (!result) return null; @@ -121,10 +124,16 @@ export function CatalogSearchResultListItem( {result.kind && } {result.type && } {result.lifecycle && ( - + )} {result.owner && ( - + )}
    diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index b1590bdff3..e18467249a 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -48,6 +48,8 @@ import { columnFactories } from './columns'; import { CatalogTableColumnsFunc, CatalogTableRow } from './types'; import { PaginatedCatalogTable } from './PaginatedCatalogTable'; import { defaultCatalogTableColumnsFunc } from './defaultCatalogTableColumnsFunc'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { catalogTranslationRef } from '../../translation'; /** * Props for {@link CatalogTable}. @@ -96,13 +98,14 @@ export const CatalogTable = (props: CatalogTableProps) => { typeof columns === 'function' ? columns(entityListContext) : columns, [columns, entityListContext], ); + const { t } = useTranslationRef(catalogTranslationRef); if (error) { return (
    @@ -113,7 +116,7 @@ export const CatalogTable = (props: CatalogTableProps) => { const defaultActions: TableProps['actions'] = [ ({ entity }) => { const url = entity.metadata.annotations?.[ANNOTATION_VIEW_URL]; - const title = 'View'; + const title = t('catalogTable.viewActionTitle'); return { icon: () => ( @@ -132,7 +135,7 @@ export const CatalogTable = (props: CatalogTableProps) => { }, ({ entity }) => { const url = entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; - const title = 'Edit'; + const title = t('catalogTable.editActionTitle'); return { icon: () => ( @@ -151,7 +154,9 @@ export const CatalogTable = (props: CatalogTableProps) => { }, ({ entity }) => { const isStarred = isStarredEntity(entity); - const title = isStarred ? 'Remove from favorites' : 'Add to favorites'; + const title = isStarred + ? t('catalogTable.unStarActionTitle') + : t('catalogTable.starActionTitle'); return { cellStyle: { paddingLeft: '1em' }, diff --git a/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.test.tsx index 976cdff667..c96fe0e067 100644 --- a/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React, { ReactNode } from 'react'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import { PaginatedCatalogTable } from './PaginatedCatalogTable'; import { screen } from '@testing-library/react'; import { CatalogTableRow } from './types'; @@ -63,8 +63,8 @@ describe('PaginatedCatalogTable', () => { ); }; - it('should display all the items', () => { - render( + it('should display all the items', async () => { + await renderInTestApp( wrapInContext(), ); @@ -74,7 +74,7 @@ describe('PaginatedCatalogTable', () => { }); it('should display and invoke the next button', async () => { - const { rerender } = render( + const { rerender } = await renderInTestApp( wrapInContext( { }); it('should display and invoke the prev button', async () => { - const { rerender } = render( + const { rerender } = await renderInTestApp( wrapInContext( diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx index f4ea850b9b..fe56ea582b 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx @@ -27,6 +27,8 @@ import { componentEntityHelpLink, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface DependsOnComponentsCardProps { @@ -37,9 +39,10 @@ export interface DependsOnComponentsCardProps { } export function DependsOnComponentsCard(props: DependsOnComponentsCardProps) { + const { t } = useTranslationRef(catalogTranslationRef); const { variant = 'gridItem', - title = 'Depends on components', + title = t('dependsOnComponentsCard.title'), columns = componentEntityColumns, tableOptions = {}, } = props; @@ -50,7 +53,7 @@ export function DependsOnComponentsCard(props: DependsOnComponentsCardProps) { entityKind="Component" relationType={RELATION_DEPENDS_ON} columns={columns} - emptyMessage="No component is a dependency of this component" + emptyMessage={t('dependsOnComponentsCard.emptyMessage')} emptyHelpLink={componentEntityHelpLink} asRenderableEntities={asComponentEntities} tableOptions={tableOptions} diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx index d99a9df387..281bfd8766 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx @@ -27,6 +27,8 @@ import { RelatedEntitiesCard, resourceEntityColumns, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface DependsOnResourcesCardProps { @@ -37,9 +39,10 @@ export interface DependsOnResourcesCardProps { } export function DependsOnResourcesCard(props: DependsOnResourcesCardProps) { + const { t } = useTranslationRef(catalogTranslationRef); const { variant = 'gridItem', - title = 'Depends on resources', + title = t('dependsOnResourcesCard.title'), columns = resourceEntityColumns, tableOptions = {}, } = props; @@ -50,7 +53,7 @@ export function DependsOnResourcesCard(props: DependsOnResourcesCardProps) { entityKind="Resource" relationType={RELATION_DEPENDS_ON} columns={columns} - emptyMessage="No resource is a dependency of this component" + emptyMessage={t('dependsOnResourcesCard.emptyMessage')} emptyHelpLink={componentEntityHelpLink} asRenderableEntities={asResourceEntities} tableOptions={tableOptions} diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index a173773e35..5b740038a1 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -33,6 +33,8 @@ import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common/ import { UnregisterEntity, UnregisterEntityOptions } from './UnregisterEntity'; import { useApi, alertApiRef } from '@backstage/core-plugin-api'; import useCopyToClipboard from 'react-use/esm/useCopyToClipboard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export type EntityContextMenuClassKey = 'button'; @@ -70,6 +72,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) { onUnregisterEntity, onInspectEntity, } = props; + const { t } = useTranslationRef(catalogTranslationRef); const [anchorEl, setAnchorEl] = useState(); const classes = useStyles(); const unregisterPermission = useEntityPermission( @@ -90,12 +93,12 @@ export function EntityContextMenu(props: EntityContextMenuProps) { useEffect(() => { if (!copyState.error && copyState.value) { alertApi.post({ - message: 'Copied!', + message: t('entityContextMenu.copiedMessage'), severity: 'info', display: 'transient', }); } - }, [copyState, alertApi]); + }, [copyState, alertApi, t]); const extraItems = UNSTABLE_extraContextMenuItems && [ ...UNSTABLE_extraContextMenuItems.map(item => ( @@ -117,7 +120,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) { return ( <> - + - + { @@ -168,7 +171,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) { - + diff --git a/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx b/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx index 3de890097b..08496d4819 100644 --- a/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx @@ -19,6 +19,8 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import MenuItem from '@material-ui/core/MenuItem'; import CancelIcon from '@material-ui/icons/Cancel'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; type VisibleType = 'visible' | 'hidden' | 'disable'; @@ -40,6 +42,7 @@ export function UnregisterEntity(props: UnregisterEntityProps) { onUnregisterEntity, onClose, } = props; + const { t } = useTranslationRef(catalogTranslationRef); const isBoolean = typeof unregisterEntityOptions?.disableUnregister === 'boolean'; @@ -65,7 +68,7 @@ export function UnregisterEntity(props: UnregisterEntityProps) { - + ); } diff --git a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx index 0b1448bb1f..d48b45f054 100644 --- a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx +++ b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx @@ -25,6 +25,8 @@ import { import { EntityLabelsEmptyState } from './EntityLabelsEmptyState'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface EntityLabelsCardProps { @@ -42,6 +44,7 @@ export const EntityLabelsCard = (props: EntityLabelsCardProps) => { const { variant, title } = props; const { entity } = useEntity(); const classes = useStyles(); + const { t } = useTranslationRef(catalogTranslationRef); const columns: TableColumn<{ key: string; value: string }>[] = [ { @@ -61,7 +64,7 @@ export const EntityLabelsCard = (props: EntityLabelsCardProps) => { const labels = entity?.metadata?.labels; return ( - + {!labels || Object.keys(labels).length === 0 ? ( ) : ( diff --git a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx index 5f5db0baa5..16d0a3a897 100644 --- a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx +++ b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx @@ -19,6 +19,8 @@ import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { CodeSnippet } from '@backstage/core-components'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const ENTITY_YAML = `metadata: name: example @@ -39,12 +41,12 @@ const useStyles = makeStyles( export function EntityLabelsEmptyState() { const classes = useStyles(); + const { t } = useTranslationRef(catalogTranslationRef); return ( <> - No labels defined for this entity. You can add labels to your entity - YAML as shown in the highlighted example below: + {t('entityLabelsCard.emptyDescription')}
    - Read more + {t('entityLabelsCard.readMoreButtonTitle')} ); diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 274972283f..bbc7792acb 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -53,6 +53,8 @@ import React, { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export type EntityLayoutRouteProps = { @@ -117,11 +119,12 @@ function headerProps( function EntityLabels(props: { entity: Entity }) { const { entity } = props; const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); + const { t } = useTranslationRef(catalogTranslationRef); return ( <> {ownedByRelations.length > 0 && ( )} @@ -234,6 +237,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { const navigate = useNavigate(); const catalogRoute = useRouteRef(rootRouteRef); const unregisterRedirectRoute = useRouteRef(unregisterRedirectRouteRef); + const { t } = useTranslationRef(catalogTranslationRef); const cleanUpAfterRemoval = async () => { setConfirmationDialogOpen(false); @@ -286,7 +290,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { {NotFoundComponent ? ( NotFoundComponent ) : ( - + There is no {kind} with the requested{' '} kind, namespace, and name diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx index ebc551c29f..b5d8269a0b 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -22,6 +22,8 @@ import { LinksGridList } from './LinksGridList'; import { ColumnBreakpoints } from './types'; import { IconComponent, useApp } from '@backstage/core-plugin-api'; import { InfoCard, InfoCardVariants } from '@backstage/core-components'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { catalogTranslationRef } from '../../translation'; /** @public */ export interface EntityLinksCardProps { @@ -33,6 +35,7 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => { const { cols = undefined, variant } = props; const { entity } = useEntity(); const app = useApp(); + const { t } = useTranslationRef(catalogTranslationRef); const iconResolver = (key?: string): IconComponent => key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon; @@ -40,7 +43,7 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => { const links = entity?.metadata?.links; return ( - + {!links || links.length === 0 ? ( ) : ( diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx index 81009c5393..09362385f9 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx @@ -19,6 +19,8 @@ import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { CodeSnippet } from '@backstage/core-components'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { catalogTranslationRef } from '../../translation'; const ENTITY_YAML = `metadata: name: example @@ -44,12 +46,12 @@ const useStyles = makeStyles( export function EntityLinksEmptyState() { const classes = useStyles(); + const { t } = useTranslationRef(catalogTranslationRef); return ( <> - No links defined for this entity. You can add links to your entity YAML - as shown in the highlighted example below: + {t('entityLinksCard.emptyDescription')}
    - Read more + {t('entityLinksCard.readMoreButtonTitle')} ); diff --git a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx index 479edf3984..252005dff5 100644 --- a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx +++ b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx @@ -20,6 +20,8 @@ import Button from '@material-ui/core/Button'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import { Illo } from './Illo'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles(theme => ({ container: { @@ -45,24 +47,24 @@ const useStyles = makeStyles(theme => ({ export function EntityNotFound() { const classes = useStyles(); + const { t } = useTranslationRef(catalogTranslationRef); return ( - Entity was not found + {t('entityNotFound.title')} - Want to help us build this? Check out our Getting Started - documentation. + {t('entityNotFound.description')} diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx index 603f2040d9..c9c24a14a1 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx @@ -23,6 +23,8 @@ import DialogTitle from '@material-ui/core/DialogTitle'; import React, { useState } from 'react'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; interface DeleteEntityDialogProps { open: boolean; @@ -36,6 +38,7 @@ export function DeleteEntityDialog(props: DeleteEntityDialogProps) { const [busy, setBusy] = useState(false); const catalogApi = useApi(catalogApiRef); const alertApi = useApi(alertApiRef); + const { t } = useTranslationRef(catalogTranslationRef); const onDelete = async () => { setBusy(true); @@ -54,7 +57,7 @@ export function DeleteEntityDialog(props: DeleteEntityDialogProps) { return ( - Are you sure you want to delete this entity? + {t('deleteEntity.dialogTitle')} diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx index 8a17559681..10a10e54fd 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx @@ -22,6 +22,8 @@ import { useNavigate } from 'react-router-dom'; import { DeleteEntityDialog } from './DeleteEntityDialog'; import { useRouteRef } from '@backstage/core-plugin-api'; import { rootRouteRef } from '../../routes'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** * Returns true if the given entity has the orphan annotation given by the @@ -44,6 +46,7 @@ export function EntityOrphanWarning() { const catalogLink = useRouteRef(rootRouteRef); const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const { entity } = useEntity(); + const { t } = useTranslationRef(catalogTranslationRef); const cleanUpAfterRemoval = async () => { setConfirmationDialogOpen(false); @@ -53,8 +56,7 @@ export function EntityOrphanWarning() { return ( <> setConfirmationDialogOpen(true)}> - This entity is not referenced by any location and is therefore not - receiving updates. Click here to delete. + {t('deleteEntity.description')} i.error && @@ -95,6 +97,7 @@ export function EntityProcessingErrorsPanel() { const { loading, error, value } = useAsync(async () => { return getOwnAndAncestorsErrors(entityRef, catalogApi); }, [entityRef, catalogApi]); + const { t } = useTranslationRef(catalogTranslationRef); if (error) { return ( @@ -115,7 +118,7 @@ export function EntityProcessingErrorsPanel() { {stringifyEntityRef(entity) !== stringifyEntityRef(ancestorError.entity) && ( - The error below originates from{' '} + {t('entityProcessingErrorsDescription')}{' '} )} diff --git a/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx index 13542a32b4..2ce9120bf0 100644 --- a/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx +++ b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx @@ -26,6 +26,8 @@ import useAsync from 'react-use/esm/useAsync'; import Box from '@material-ui/core/Box'; import { ResponseErrorPanel } from '@backstage/core-components'; import { useApi, ApiHolder } from '@backstage/core-plugin-api'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; async function getRelationWarnings(entity: Entity, catalogApi: CatalogApi) { const entityRefRelations = entity.relations?.map( @@ -80,6 +82,7 @@ export function EntityRelationWarning() { const { loading, error, value } = useAsync(async () => { return getRelationWarnings(entity, catalogApi); }, [entity, catalogApi]); + const { t } = useTranslationRef(catalogTranslationRef); if (error) { return ( @@ -95,10 +98,8 @@ export function EntityRelationWarning() { return ( <> - - This entity has relations to other entities, which can't be found in the - catalog.
    - Entities not found are: {value.join(', ')} + + {t('entityRelationWarningDescription')} {value.join(', ')} ); diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx index 9071dac030..917543dd12 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -23,6 +23,8 @@ import { componentEntityHelpLink, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasComponentsCardProps { @@ -31,7 +33,8 @@ export interface HasComponentsCardProps { } export function HasComponentsCard(props: HasComponentsCardProps) { - const { variant = 'gridItem', title = 'Has components' } = props; + const { t } = useTranslationRef(catalogTranslationRef); + const { variant = 'gridItem', title = t('hasComponentsCard.title') } = props; return ( diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx index 5af7eec258..d11f10c882 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx @@ -23,6 +23,8 @@ import { resourceEntityColumns, resourceEntityHelpLink, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasResourcesCardProps { @@ -31,7 +33,8 @@ export interface HasResourcesCardProps { } export function HasResourcesCard(props: HasResourcesCardProps) { - const { variant = 'gridItem', title = 'Has resources' } = props; + const { t } = useTranslationRef(catalogTranslationRef); + const { variant = 'gridItem', title = t('hasResourcesCard.title') } = props; return ( ); diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx index ac40c82435..d81349794a 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx @@ -22,6 +22,8 @@ import { componentEntityColumns, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasSubcomponentsCardProps { @@ -31,10 +33,11 @@ export interface HasSubcomponentsCardProps { } export function HasSubcomponentsCard(props: HasSubcomponentsCardProps) { + const { t } = useTranslationRef(catalogTranslationRef); const { variant = 'gridItem', tableOptions = {}, - title = 'Has subcomponents', + title = t('hasSubcomponentsCard.title'), } = props; return ( diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx index d6da465ffa..cee45a95c5 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx @@ -23,6 +23,8 @@ import { systemEntityColumns, systemEntityHelpLink, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasSystemsCardProps { @@ -31,7 +33,8 @@ export interface HasSystemsCardProps { } export function HasSystemsCard(props: HasSystemsCardProps) { - const { variant = 'gridItem', title = 'Has systems' } = props; + const { t } = useTranslationRef(catalogTranslationRef); + const { variant = 'gridItem', title = t('hasSystemsCard.title') } = props; return ( ); diff --git a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx index eb37973c27..dd1359482d 100644 --- a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx +++ b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx @@ -31,6 +31,8 @@ import { TableColumn, TableOptions, } from '@backstage/core-components'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export type RelatedEntitiesCardProps = { @@ -72,6 +74,7 @@ export function RelatedEntitiesCard( tableOptions = {}, } = props; + const { t } = useTranslationRef(catalogTranslationRef); const { entity } = useEntity(); const { entities, loading, error } = useRelatedEntities(entity, { type: relationType, @@ -102,7 +105,9 @@ export function RelatedEntitiesCard(
    {emptyMessage} - Learn how to change this. + + {t('relatedEntitiesCard.emptyHelpLinkTitle')} +
    } diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx index 22466b1464..5e485d672b 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx @@ -46,6 +46,8 @@ import { } from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export type SystemDiagramCardClassKey = @@ -164,6 +166,7 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps) { export function SystemDiagramCard() { const { entity } = useEntity(); const theme = useTheme(); + const { t } = useTranslationRef(catalogTranslationRef); const currentSystemName = entity.metadata.name; const currentSystemNode = stringifyEntityRef(entity); const systemNodes = new Array<{ id: string; kind: string; name: string }>(); @@ -211,7 +214,7 @@ export function SystemDiagramCard() { systemEdges.push({ from: currentSystemNode, to: stringifyEntityRef(foundDomain), - label: 'part of', + label: t('systemDiagramCard.edgeLabels.partOf'), }), ); @@ -235,7 +238,7 @@ export function SystemDiagramCard() { systemEdges.push({ from: stringifyEntityRef(catalogItem), to: stringifyEntityRef(foundRelation), - label: 'part of', + label: t('systemDiagramCard.edgeLabels.partOf'), }), ); @@ -247,7 +250,7 @@ export function SystemDiagramCard() { systemEdges.push({ from: stringifyEntityRef(catalogItem), to: stringifyEntityRef(foundRelation), - label: 'provides', + label: t('systemDiagramCard.edgeLabels.provides'), }), ); @@ -259,7 +262,7 @@ export function SystemDiagramCard() { systemEdges.push({ from: stringifyEntityRef(catalogItem), to: stringifyEntityRef(foundRelation), - label: 'depends on', + label: t('systemDiagramCard.edgeLabels.dependsOn'), }), ); } @@ -272,7 +275,7 @@ export function SystemDiagramCard() { } return ( - + - Use pinch & zoom - to move around the diagram. + + {t('systemDiagramCard.description')} ); diff --git a/plugins/catalog/src/translation.ts b/plugins/catalog/src/translation.ts index bd27069fde..2699b58542 100644 --- a/plugins/catalog/src/translation.ts +++ b/plugins/catalog/src/translation.ts @@ -23,6 +23,141 @@ export const catalogTranslationRef = createTranslationRef({ indexPage: { title: `{{orgName}} Catalog`, createButtonTitle: 'Create', + supportButtonContent: 'All your software catalog entities', + }, + aboutCard: { + title: 'About', + refreshButtonTitle: 'Schedule entity refresh', + editButtonTitle: 'Edit Metadata', + createSimilarButtonTitle: 'Create something similar', + refreshScheduledMessage: 'Refresh scheduled', + launchTemplate: 'Launch Template', + viewTechdocs: 'View TechDocs', + viewSource: 'View Source', + descriptionField: { + label: 'Description', + value: 'No description', + }, + ownerField: { + label: 'Owner', + value: 'No Owner', + }, + domainField: { + label: 'Domain', + value: 'No Domain', + }, + systemField: { + label: 'System', + value: 'No System', + }, + parentComponentField: { + label: 'Parent Component', + value: 'No Parent Component', + }, + typeField: { + label: 'Type', + }, + lifecycleField: { + label: 'Lifecycle', + }, + tagsField: { + label: 'Tags', + value: 'No Tags', + }, + targetsField: { + label: 'Targets', + }, + }, + searchResultItem: { + lifecycle: 'Lifecycle', + Owner: 'Owner', + }, + catalogTable: { + warningPanelTitle: 'Could not fetch catalog entities.', + viewActionTitle: 'View', + editActionTitle: 'Edit', + starActionTitle: 'Add to favorites', + unStarActionTitle: 'Remove from favorites', + }, + dependencyOfComponentsCard: { + title: 'Dependency of components', + emptyMessage: 'No component depends on this component', + }, + dependsOnComponentsCard: { + title: 'Depends on components', + emptyMessage: 'No component is a dependency of this component', + }, + dependsOnResourcesCard: { + title: 'Depends on resources', + emptyMessage: 'No resource is a dependency of this component', + }, + entityContextMenu: { + copiedMessage: 'Copied!', + moreButtonTitle: 'More', + inspectMenuTitle: 'Inspect entity', + copyURLMenuTitle: 'Copy entity URL', + unregisterMenuTitle: 'Unregister entity', + }, + entityLabelsCard: { + title: 'Labels', + emptyDescription: + 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:', + readMoreButtonTitle: 'Read more', + }, + entityLabels: { + warningPanelTitle: 'Entity not found', + ownerLabel: 'Owner', + lifecycleLabel: 'Lifecycle', + }, + entityLinksCard: { + title: 'Links', + emptyDescription: + 'No links defined for this entity. You can add links to your entity YAML as shown in the highlighted example below:', + readMoreButtonTitle: 'Read more', + }, + entityNotFound: { + title: 'Entity was not found', + description: + 'Want to help us build this? Check out our Getting Started documentation.', + docButtonTitle: 'DOCS', + }, + deleteEntity: { + dialogTitle: 'Are you sure you want to delete this entity?', + deleteButtonTitle: 'Delete', + cancelButtonTitle: 'Cancel', + description: + 'This entity is not referenced by any location and is therefore not receiving updates. Click here to delete.', + }, + entityProcessingErrorsDescription: 'The error below originates from', + entityRelationWarningDescription: + "This entity has relations to other entities, which can't be found in the catalog.\n Entities not found are: ", + hasComponentsCard: { + title: 'Has components', + emptyMessage: 'No component is part of this system', + }, + hasResourcesCard: { + title: 'Has resources', + emptyMessage: 'No resource is part of this system', + }, + hasSubcomponentsCard: { + title: 'Has subcomponents', + emptyMessage: 'No subcomponent is part of this component', + }, + hasSystemsCard: { + title: 'Has systems', + emptyMessage: 'No system is part of this domain', + }, + relatedEntitiesCard: { + emptyHelpLinkTitle: 'Learn how to change this.', + }, + systemDiagramCard: { + title: 'System Diagram', + description: 'Use pinch & zoo to move around the diagram.', + edgeLabels: { + partOf: 'part of', + provides: 'provides', + dependsOn: 'depends on', + }, }, }, }); diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx index 5aa138340e..90157c3226 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent } from '@testing-library/react'; import { CardHeader } from './CardHeader'; import { ThemeProvider } from '@material-ui/core/styles'; import { lightTheme } from '@backstage/theme'; @@ -30,7 +30,7 @@ import { stringifyEntityRef } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; describe('CardHeader', () => { - it('should select the correct theme from the theme provider from the header', () => { + it('should select the correct theme from the theme provider from the header', async () => { // Can't really test what we want here. // But we can check that we call the getPage theme with the right type of template at least. const mockTheme = { @@ -38,7 +38,7 @@ describe('CardHeader', () => { getPageTheme: jest.fn(lightTheme.getPageTheme), }; - render( + await renderInTestApp( [] | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; } >; @@ -110,8 +110,6 @@ export const EntityPickerFieldSchema: FieldSchema< { defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; - allowedKinds?: string[] | undefined; - allowArbitraryValues?: boolean | undefined; catalogFilter?: | Record< string, @@ -130,6 +128,8 @@ export const EntityPickerFieldSchema: FieldSchema< } >[] | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; } >; @@ -216,7 +216,6 @@ export const MultiEntityPickerFieldExtension: FieldExtensionComponent_2< { defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; - allowArbitraryValues?: boolean | undefined; catalogFilter?: | Record< string, @@ -235,6 +234,7 @@ export const MultiEntityPickerFieldExtension: FieldExtensionComponent_2< } >[] | undefined; + allowArbitraryValues?: boolean | undefined; } >; @@ -269,8 +269,6 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2< { defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; - allowedKinds?: string[] | undefined; - allowArbitraryValues?: boolean | undefined; catalogFilter?: | Record< string, @@ -289,6 +287,8 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent_2< } >[] | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; } >; @@ -298,8 +298,6 @@ export const OwnedEntityPickerFieldSchema: FieldSchema< { defaultKind?: string | undefined; defaultNamespace?: string | false | undefined; - allowedKinds?: string[] | undefined; - allowArbitraryValues?: boolean | undefined; catalogFilter?: | Record< string, @@ -318,6 +316,8 @@ export const OwnedEntityPickerFieldSchema: FieldSchema< } >[] | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; } >; @@ -330,8 +330,6 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent_2< string, { defaultNamespace?: string | false | undefined; - allowedKinds?: string[] | undefined; - allowArbitraryValues?: boolean | undefined; catalogFilter?: | Record< string, @@ -350,6 +348,8 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent_2< } >[] | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; } >; @@ -358,8 +358,6 @@ export const OwnerPickerFieldSchema: FieldSchema< string, { defaultNamespace?: string | false | undefined; - allowedKinds?: string[] | undefined; - allowArbitraryValues?: boolean | undefined; catalogFilter?: | Record< string, @@ -378,6 +376,8 @@ export const OwnerPickerFieldSchema: FieldSchema< } >[] | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; } >;