From 66a1140dd8ef6aeac230e9425caca14462a5e1fb Mon Sep 17 00:00:00 2001 From: JounQin Date: Wed, 11 Dec 2024 18:07:38 +0800 Subject: [PATCH] feat: add i18n support for catalog-import plugin Signed-off-by: JounQin --- .changeset/neat-glasses-occur.md | 5 + plugins/catalog-import/report-alpha.api.md | 63 +++++++- plugins/catalog-import/report.api.md | 3 + plugins/catalog-import/src/alpha.tsx | 2 + .../src/api/AzureRepoApiClient.test.ts | 4 +- .../src/components/Buttons/index.tsx | 6 +- .../DefaultImportPage/DefaultImportPage.tsx | 19 ++- .../ImportInfoCard/ImportInfoCard.tsx | 27 ++-- .../ImportStepper/ImportStepper.tsx | 16 +- .../src/components/ImportStepper/defaults.tsx | 116 ++++++++++---- .../StepFinishImportLocation.tsx | 34 ++-- .../StepInitAnalyzeUrl.test.tsx | 100 +++++++----- .../StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx | 22 +-- .../PreviewPullRequestComponent.test.tsx | 19 ++- .../PreviewPullRequestComponent.tsx | 11 +- .../StepPrepareCreatePullRequest.test.tsx | 148 +++++++++--------- .../StepPrepareCreatePullRequest.tsx | 14 +- .../StepPrepareSelectLocations.tsx | 20 ++- .../StepReviewLocation/StepReviewLocation.tsx | 26 +-- plugins/catalog-import/src/translation.ts | 125 ++++++++++++++- plugins/scaffolder-react/report-alpha.api.md | 2 +- 21 files changed, 552 insertions(+), 230 deletions(-) create mode 100644 .changeset/neat-glasses-occur.md diff --git a/.changeset/neat-glasses-occur.md b/.changeset/neat-glasses-occur.md new file mode 100644 index 0000000000..4e5e62b7ca --- /dev/null +++ b/.changeset/neat-glasses-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-import': patch +--- + +Add i18n support for catalog-import plugin diff --git a/plugins/catalog-import/report-alpha.api.md b/plugins/catalog-import/report-alpha.api.md index 24789a42e4..d4dab4549a 100644 --- a/plugins/catalog-import/report-alpha.api.md +++ b/plugins/catalog-import/report-alpha.api.md @@ -16,8 +16,69 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha'; export const catalogImportTranslationRef: TranslationRef< 'catalog-import', { - readonly pageTitle: 'Register an existing component'; + readonly 'buttons.back': 'Back'; + readonly 'defaultImportPage.headerTitle': 'Register an existing component'; + readonly 'defaultImportPage.contentHeaderTitle': 'Start tracking your component in {{appTitle}}'; + readonly 'defaultImportPage.supportTitle': 'Start tracking your component in {{appTitle}} by adding it to the software catalog.'; readonly 'importInfoCard.title': 'Register an existing component'; + readonly 'importInfoCard.deepLinkTitle': 'Learn more about the Software Catalog'; + readonly 'importInfoCard.linkDescription': 'Enter the URL to your source code repository to add it to {{appTitle}}.'; + readonly 'importInfoCard.fileLinkTitle': 'Link to an existing entity file'; + readonly 'importInfoCard.examplePrefix': 'Example: '; + readonly 'importInfoCard.fileLinkDescription': 'The wizard analyzes the file, previews the entities, and adds them to the {{appTitle}} catalog.'; + readonly 'importInfoCard.githubIntegration.label': 'GitHub only'; + readonly 'importInfoCard.githubIntegration.title': 'Link to a repository'; + readonly 'importStepper.finish.title': 'Finish'; + readonly 'importStepper.noLocation.title': 'Create Pull Request'; + readonly 'importStepper.noLocation.createPr.ownerLabel': 'Entity Owner'; + readonly 'importStepper.noLocation.createPr.detailsTitle': 'Pull Request Details'; + readonly 'importStepper.noLocation.createPr.titleLabel': 'Pull Request Title'; + readonly 'importStepper.noLocation.createPr.titlePlaceholder': 'Add Backstage catalog entity descriptor files'; + readonly 'importStepper.noLocation.createPr.bodyLabel': 'Pull Request Body'; + readonly 'importStepper.noLocation.createPr.bodyPlaceholder': 'A describing text with Markdown support'; + readonly 'importStepper.noLocation.createPr.configurationTitle': 'Entity Configuration'; + readonly 'importStepper.noLocation.createPr.componentNameLabel': 'Name of the created component'; + readonly 'importStepper.noLocation.createPr.componentNamePlaceholder': 'my-component'; + readonly 'importStepper.noLocation.createPr.ownerLoadingText': 'Loading groups…'; + readonly 'importStepper.noLocation.createPr.ownerHelperText': 'Select an owner from the list or enter a reference to a Group or a User'; + readonly 'importStepper.noLocation.createPr.ownerErrorHelperText': 'required value'; + readonly 'importStepper.noLocation.createPr.ownerPlaceholder': 'my-group'; + readonly 'importStepper.noLocation.createPr.codeownersHelperText': 'WARNING: This may fail if no CODEOWNERS file is found at the target location.'; + readonly 'importStepper.singleLocation.title': 'Select Locations'; + readonly 'importStepper.singleLocation.description': 'Discovered Locations: 1'; + readonly 'importStepper.multipleLocations.title': 'Select Locations'; + readonly 'importStepper.multipleLocations.description': 'Discovered Locations: {{length, number}}'; + readonly 'importStepper.analyze.title': 'Select URL'; + readonly 'importStepper.prepare.title': 'Import Actions'; + readonly 'importStepper.prepare.description': 'Optional'; + readonly 'importStepper.review.title': 'Review'; + readonly 'stepFinishImportLocation.repository.title': 'The following Pull Request has been opened: '; + readonly 'stepFinishImportLocation.repository.description': 'Your entities will be imported as soon as the Pull Request is merged.'; + readonly 'stepFinishImportLocation.backButtonText': 'Register another'; + readonly 'stepFinishImportLocation.locations.new': 'The following entities have been added to the catalog:'; + readonly 'stepFinishImportLocation.locations.backButtonText': 'Register another'; + readonly 'stepFinishImportLocation.locations.existing': 'A refresh was triggered for the following locations:'; + readonly 'stepFinishImportLocation.locations.viewButtonText': 'View Component'; + readonly 'stepInitAnalyzeUrl.error.default': 'Received unknown analysis result of type {{type}}. Please contact the support team.'; + readonly 'stepInitAnalyzeUrl.error.url': 'Must start with http:// or https://.'; + readonly 'stepInitAnalyzeUrl.error.repository': "Couldn't generate entities for your repository"; + readonly 'stepInitAnalyzeUrl.error.locations': 'There are no entities at this location'; + readonly 'stepInitAnalyzeUrl.urlHelperText': 'Enter the full path to your entity file to start tracking your component'; + readonly 'stepInitAnalyzeUrl.nextButtonText': 'Analyze'; + readonly 'stepPrepareCreatePullRequest.nextButtonText': 'Create PR'; + readonly 'stepPrepareCreatePullRequest.previewPr.title': 'Preview Pull Request'; + readonly 'stepPrepareCreatePullRequest.previewPr.subheader': 'Create a new Pull Request'; + readonly 'stepPrepareCreatePullRequest.previewCatalogInfo.title': 'Preview Entities'; + readonly 'stepPrepareSelectLocations.locations.description': 'Select one or more locations that are present in your git repository:'; + readonly 'stepPrepareSelectLocations.locations.selectAll': 'Select All'; + readonly 'stepPrepareSelectLocations.nextButtonText': 'Review'; + readonly 'stepPrepareSelectLocations.existingLocations.description': 'These locations already exist in the catalog:'; + readonly 'stepReviewLocation.refresh': 'Refresh'; + readonly 'stepReviewLocation.import': 'Import'; + readonly 'stepReviewLocation.catalog.new': 'The following entities will be added to the catalog:'; + readonly 'stepReviewLocation.catalog.exists': 'The following locations already exist in the catalog:'; + readonly 'stepReviewLocation.prepareResult.title': 'The following Pull Request has been opened: '; + readonly 'stepReviewLocation.prepareResult.description': 'You can already import the location and {{appTitle}} will fetch the entities as soon as the Pull Request is merged.'; } >; diff --git a/plugins/catalog-import/report.api.md b/plugins/catalog-import/report.api.md index 9bc71a4296..eed2639168 100644 --- a/plugins/catalog-import/report.api.md +++ b/plugins/catalog-import/report.api.md @@ -6,6 +6,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; +import { catalogImportTranslationRef } from '@backstage/plugin-catalog-import/alpha'; import { ComponentProps } from 'react'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConfigApi } from '@backstage/core-plugin-api'; @@ -24,6 +25,7 @@ import { ScmAuthApi } from '@backstage/integration-react'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { SubmitHandler } from 'react-hook-form'; import { TextFieldProps } from '@material-ui/core/TextField/TextField'; +import { TranslationFunction } from '@backstage/core-plugin-api/alpha'; import { UseFormProps } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form'; @@ -145,6 +147,7 @@ export { catalogImportPlugin as plugin }; export function defaultGenerateStepper( flow: ImportFlows, defaults: StepperProvider, + t: TranslationFunction, ): StepperProvider; // @public diff --git a/plugins/catalog-import/src/alpha.tsx b/plugins/catalog-import/src/alpha.tsx index 0d1599ee5c..3fade2d92b 100644 --- a/plugins/catalog-import/src/alpha.tsx +++ b/plugins/catalog-import/src/alpha.tsx @@ -92,3 +92,5 @@ export default createFrontendPlugin({ importPage: convertLegacyRouteRef(rootRouteRef), }, }); + +export { catalogImportTranslationRef } from './translation'; diff --git a/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts b/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts index 9f058dc744..00e77461b6 100644 --- a/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts +++ b/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts @@ -154,7 +154,7 @@ function mockPrEndpoint() { describe('RepoApiClient', () => { const server = setupServer(); registerMswTestHooks(server); - const testToken = new Date().toString(); + const testToken = new Date().toLocaleString('en-US'); const sut = new RepoApiClient({ project: 'project', tenantUrl: 'https://dev.azure.com/acme', @@ -316,7 +316,7 @@ describe('createAzurePullRequest', () => { }); it('should create a new Pull request', async () => { - const testToken = new Date().getTime().toString(); + const testToken = new Date().toLocaleString('en-US'); const options: AzurePrOptions = { tenantUrl: 'https://dev.azure.com/acme', repository: 'test', diff --git a/plugins/catalog-import/src/components/Buttons/index.tsx b/plugins/catalog-import/src/components/Buttons/index.tsx index 6ba5a83fb5..f709f65a75 100644 --- a/plugins/catalog-import/src/components/Buttons/index.tsx +++ b/plugins/catalog-import/src/components/Buttons/index.tsx @@ -15,11 +15,14 @@ */ import { LinkButton } from '@backstage/core-components'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; import Button from '@material-ui/core/Button'; import CircularProgress from '@material-ui/core/CircularProgress'; import { makeStyles } from '@material-ui/core/styles'; import { ComponentProps } from 'react'; +import { catalogImportTranslationRef } from '../../translation'; + const useStyles = makeStyles(theme => ({ wrapper: { marginTop: theme.spacing(1), @@ -62,11 +65,12 @@ export const NextButton = ( }; export const BackButton = (props: ComponentProps) => { + const { t } = useTranslationRef(catalogImportTranslationRef); const classes = useStyles(); return ( ); }; diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx index 18ffe2bd8d..24a79866b3 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx @@ -22,13 +22,14 @@ import { SupportButton, } from '@backstage/core-components'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; import Grid from '@material-ui/core/Grid'; -import useMediaQuery from '@material-ui/core/useMediaQuery'; import { useTheme } from '@material-ui/core/styles'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; + +import { catalogImportTranslationRef } from '../../translation'; import { ImportInfoCard } from '../ImportInfoCard'; import { ImportStepper } from '../ImportStepper'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { catalogImportTranslationRef } from '../../translation'; /** * The default catalog import page. @@ -42,8 +43,6 @@ export const DefaultImportPage = () => { const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const appTitle = configApi.getOptionalString('app.title') || 'Backstage'; - const supportTitle = `Start tracking your component in ${appTitle} by adding it to the software catalog.`; - const contentItems = [ @@ -56,10 +55,14 @@ export const DefaultImportPage = () => { return ( -
+
- - {supportTitle} + + + {t('defaultImportPage.supportTitle', { appTitle })} + diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx index fc15d01538..1777c0786b 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx @@ -16,11 +16,12 @@ import { InfoCard } from '@backstage/core-components'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; import Chip from '@material-ui/core/Chip'; import Typography from '@material-ui/core/Typography'; + import { catalogImportApiRef } from '../../api'; import { useCatalogFilename } from '../../hooks'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { catalogImportTranslationRef } from '../../translation'; /** @@ -58,31 +59,37 @@ export const ImportInfoCard = (props: ImportInfoCardProps) => { title={t('importInfoCard.title')} titleTypographyProps={{ component: 'h3' }} deepLink={{ - title: 'Learn more about the Software Catalog', + title: t('importInfoCard.deepLinkTitle'), link: 'https://backstage.io/docs/features/software-catalog/', }} > - Enter the URL to your source code repository to add it to {appTitle}. + {t('importInfoCard.linkDescription', { appTitle })} - Link to an existing entity file + {t('importInfoCard.fileLinkTitle')} - Example: {exampleLocationUrl} + {t('importInfoCard.examplePrefix')} + {exampleLocationUrl} - The wizard analyzes the file, previews the entities, and adds them to - the {appTitle} catalog. + {t('importInfoCard.fileLinkDescription', { appTitle })} {hasGithubIntegration && ( <> - Link to a repository{' '} - + {t('importInfoCard.githubIntegration.title')} + - Example: {exampleRepositoryUrl} + {t('importInfoCard.examplePrefix')} + {exampleRepositoryUrl} The wizard discovers all {catalogFilename} files in the diff --git a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx index 30d02032b6..ed399a8a96 100644 --- a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx +++ b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx @@ -16,12 +16,15 @@ import { InfoCard, InfoCardVariants } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; import Step from '@material-ui/core/Step'; import StepContent from '@material-ui/core/StepContent'; import Stepper from '@material-ui/core/Stepper'; import { makeStyles } from '@material-ui/core/styles'; import { useMemo } from 'react'; + import { catalogImportApiRef } from '../../api'; +import { catalogImportTranslationRef } from '../../translation'; import { ImportFlows, ImportState, useImportState } from '../useImportState'; import { defaultGenerateStepper, @@ -56,6 +59,7 @@ export interface ImportStepperProps { * @public */ export const ImportStepper = (props: ImportStepperProps) => { + const { t } = useTranslationRef(catalogImportTranslationRef); const { initialUrl, generateStepper = defaultGenerateStepper, @@ -67,8 +71,8 @@ export const ImportStepper = (props: ImportStepperProps) => { const state = useImportState({ initialUrl }); const states = useMemo( - () => generateStepper(state.activeFlow, defaultStepper), - [generateStepper, state.activeFlow], + () => generateStepper(state.activeFlow, defaultStepper, t), + [generateStepper, state.activeFlow, t], ); const render = (step: StepConfiguration) => { @@ -90,25 +94,25 @@ export const ImportStepper = (props: ImportStepperProps) => { {render( states.analyze( state as Extract, - { apis: { catalogImportApi } }, + { apis: { catalogImportApi }, t }, ), )} {render( states.prepare( state as Extract, - { apis: { catalogImportApi } }, + { apis: { catalogImportApi }, t }, ), )} {render( states.review( state as Extract, - { apis: { catalogImportApi } }, + { apis: { catalogImportApi }, t }, ), )} {render( states.finish( state as Extract, - { apis: { catalogImportApi } }, + { apis: { catalogImportApi }, t }, ), )} diff --git a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx index d23ffb4db9..2b8f7e107b 100644 --- a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx +++ b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx @@ -14,6 +14,8 @@ * limitations under the License. */ +import { TranslationFunction } from '@backstage/core-plugin-api/alpha'; +import { catalogImportTranslationRef } from '@backstage/plugin-catalog-import/alpha'; import Box from '@material-ui/core/Box'; import Checkbox from '@material-ui/core/Checkbox'; import FormControlLabel from '@material-ui/core/FormControlLabel'; @@ -48,19 +50,31 @@ export type StepConfiguration = { export interface StepperProvider { analyze: ( s: Extract, - opts: { apis: StepperApis }, + opts: { + apis: StepperApis; + t: TranslationFunction; + }, ) => StepConfiguration; prepare: ( s: Extract, - opts: { apis: StepperApis }, + opts: { + apis: StepperApis; + t: TranslationFunction; + }, ) => StepConfiguration; review: ( s: Extract, - opts: { apis: StepperApis }, + opts: { + apis: StepperApis; + t: TranslationFunction; + }, ) => StepConfiguration; finish: ( s: Extract, - opts: { apis: StepperApis }, + opts: { + apis: StepperApis; + t: TranslationFunction; + }, ) => StepConfiguration; } @@ -77,6 +91,7 @@ export interface StepperProvider { export function defaultGenerateStepper( flow: ImportFlows, defaults: StepperProvider, + t: TranslationFunction, ): StepperProvider { switch (flow) { // the prepare step is skipped but the label of the step is updated @@ -88,11 +103,11 @@ export function defaultGenerateStepper( - Discovered Locations: 1 + {t('importStepper.singleLocation.description')} } > - Select Locations + {t('importStepper.singleLocation.title')} ), content: <>, @@ -113,11 +128,13 @@ export function defaultGenerateStepper( - Discovered Locations: {state.analyzeResult.locations.length} + {t('importStepper.multipleLocations.description', { + length: state.analyzeResult.locations.length, + })} } > - Select Locations + {t('importStepper.multipleLocations.title')} ), content: ( @@ -141,7 +158,9 @@ export function defaultGenerateStepper( } return { - stepLabel: Create Pull Request, + stepLabel: ( + {t('importStepper.noLocation.title')} + ), content: ( ( <> - Pull Request Details + + {t('importStepper.noLocation.createPr.detailsTitle')} + - Entity Configuration + + {t( + 'importStepper.noLocation.createPr.configurationTitle', + )} + - WARNING: This may fail if no CODEOWNERS file is found at - the target location. + {t( + 'importStepper.noLocation.createPr.codeownersHelperText', + )} )} @@ -261,8 +305,8 @@ export function defaultGenerateStepper( } export const defaultStepper: StepperProvider = { - analyze: (state, { apis }) => ({ - stepLabel: Select URL, + analyze: (state, { apis, t }) => ({ + stepLabel: {t('importStepper.analyze.title')}, content: ( ({ + prepare: (state, { t }) => ({ stepLabel: ( - Optional}> - Import Actions + + {t('importStepper.prepare.description')} + + } + > + {t('importStepper.prepare.title')} ), content: , }), - review: state => ({ - stepLabel: Review, + review: (state, { t }) => ({ + stepLabel: {t('importStepper.review.title')}, content: ( ({ - stepLabel: Finish, + finish: (state, { t }) => ({ + stepLabel: {t('importStepper.finish.title')}, content: ( { + const { t } = useTranslationRef(catalogImportTranslationRef); const entityRoute = useRouteRef(entityRouteRef); if (prepareResult.type === 'repository') { return ( <> - The following Pull Request has been opened:{' '} + {t('stepFinishImportLocation.repository.title')} { - Your entities will be imported as soon as the Pull Request is merged. + {t('stepFinishImportLocation.repository.description')} - Register another + + {t('stepFinishImportLocation.backButtonText')} + ); @@ -94,9 +100,7 @@ export const StepFinishImportLocation = ({ prepareResult, onReset }: Props) => { <> {newLocations.length > 0 && ( <> - - The following entities have been added to the catalog: - + {t('stepFinishImportLocation.locations.new')} { {existingLocations.length > 0 && ( <> - A refresh was triggered for the following locations: + {t('stepFinishImportLocation.locations.existing')} { {newComponentEntity && ( - View Component + {t('stepFinishImportLocation.locations.viewButtonText')} )} - Register another + + {t('stepFinishImportLocation.backButtonText')} + ); diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx index b1d5de7527..ed6fb5683f 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx @@ -15,8 +15,8 @@ */ import { errorApiRef } from '@backstage/core-plugin-api'; -import { TestApiProvider } from '@backstage/test-utils'; -import { act, render, screen } from '@testing-library/react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ReactNode } from 'react'; import { AnalyzeResult, catalogImportApiRef } from '../../api/'; @@ -60,23 +60,24 @@ describe('', () => { }); it('renders without exploding', async () => { - render( undefined} />, { - wrapper: Wrapper, - }); + await renderInTestApp( + + undefined} /> + , + ); expect(screen.getByRole('textbox', { name: /URL/i })).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: /URL/i })).toHaveValue(''); }); it('should use default analysis url', async () => { - render( - undefined} - analysisUrl="https://default" - />, - { - wrapper: Wrapper, - }, + await renderInTestApp( + + undefined} + analysisUrl="https://default" + /> + , ); expect(screen.getByRole('textbox', { name: /URL/i })).toBeInTheDocument(); @@ -88,9 +89,11 @@ describe('', () => { it('should not analyze without url', async () => { const onAnalysisFn = jest.fn(); - render(, { - wrapper: Wrapper, - }); + await renderInTestApp( + + + , + ); await act(async () => { try { @@ -108,9 +111,11 @@ describe('', () => { it('should not analyze invalid value', async () => { const onAnalysisFn = jest.fn(); - render(, { - wrapper: Wrapper, - }); + await renderInTestApp( + + + , + ); await act(async () => { await userEvent.type( @@ -136,9 +141,11 @@ describe('', () => { locations: [location], } as AnalyzeResult; - render(, { - wrapper: Wrapper, - }); + await renderInTestApp( + + + , + ); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve(analyzeResult), @@ -170,9 +177,11 @@ describe('', () => { locations: [location, location], } as AnalyzeResult; - render(, { - wrapper: Wrapper, - }); + await renderInTestApp( + + + , + ); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve(analyzeResult), @@ -203,9 +212,11 @@ describe('', () => { locations: [], } as AnalyzeResult; - render(, { - wrapper: Wrapper, - }); + await renderInTestApp( + + + , + ); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve(analyzeResult), @@ -244,9 +255,11 @@ describe('', () => { ], } as AnalyzeResult; - render(, { - wrapper: Wrapper, - }); + await renderInTestApp( + + + , + ); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve(analyzeResult), @@ -279,9 +292,11 @@ describe('', () => { generatedEntities: [], } as AnalyzeResult; - render(, { - wrapper: Wrapper, - }); + await renderInTestApp( + + + , + ); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve(analyzeResult), @@ -320,11 +335,10 @@ describe('', () => { ], } as AnalyzeResult; - render( - , - { - wrapper: Wrapper, - }, + await renderInTestApp( + + + , ); catalogImportApi.analyzeUrl.mockReturnValueOnce( @@ -349,9 +363,11 @@ describe('', () => { it('should report unknown type to the errorapi', async () => { const onAnalysisFn = jest.fn(); - render(, { - wrapper: Wrapper, - }); + await renderInTestApp( + + + , + ); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve({ type: 'unknown' } as any as AnalyzeResult), diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx index 2e2994aeb4..5a6cad055c 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx @@ -15,12 +15,15 @@ */ import { errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; import FormHelperText from '@material-ui/core/FormHelperText'; import Grid from '@material-ui/core/Grid'; import TextField from '@material-ui/core/TextField'; import { useCallback, useState } from 'react'; import { useForm } from 'react-hook-form'; + import { AnalyzeResult, catalogImportApiRef } from '../../api'; +import { catalogImportTranslationRef } from '../../translation'; import { NextButton } from '../Buttons'; import { asInputRef } from '../helpers'; import { ImportFlows, PrepareResult } from '../useImportState'; @@ -55,6 +58,7 @@ export interface StepInitAnalyzeUrlProps { * @public */ export const StepInitAnalyzeUrl = (props: StepInitAnalyzeUrlProps) => { + const { t } = useTranslationRef(catalogImportTranslationRef); const { onAnalysis, analysisUrl = '', @@ -95,7 +99,7 @@ export const StepInitAnalyzeUrl = (props: StepInitAnalyzeUrlProps) => { ) { onAnalysis('no-location', url, analysisResult); } else { - setError("Couldn't generate entities for your repository"); + setError(t('stepInitAnalyzeUrl.error.repository')); setSubmitted(false); } break; @@ -108,16 +112,16 @@ export const StepInitAnalyzeUrl = (props: StepInitAnalyzeUrlProps) => { } else if (analysisResult.locations.length > 1) { onAnalysis('multiple-locations', url, analysisResult); } else { - setError('There are no entities at this location'); + setError(t('stepInitAnalyzeUrl.error.locations')); setSubmitted(false); } break; } default: { - const err = `Received unknown analysis result of type ${ - (analysisResult as any).type - }. Please contact the support team.`; + const err = t('stepInitAnalyzeUrl.error.default', { + type: (analysisResult as any).type, + }); setError(err); setSubmitted(false); @@ -130,7 +134,7 @@ export const StepInitAnalyzeUrl = (props: StepInitAnalyzeUrlProps) => { setSubmitted(false); } }, - [catalogImportApi, disablePullRequest, errorApi, onAnalysis], + [catalogImportApi, disablePullRequest, errorApi, onAnalysis, t], ); return ( @@ -143,7 +147,7 @@ export const StepInitAnalyzeUrl = (props: StepInitAnalyzeUrlProps) => { httpsValidator: (value: any) => (typeof value === 'string' && value.match(/^http[s]?:\/\//) !== null) || - 'Must start with http:// or https://.', + t('stepInitAnalyzeUrl.error.url'), }, }), )} @@ -151,7 +155,7 @@ export const StepInitAnalyzeUrl = (props: StepInitAnalyzeUrlProps) => { id="url" label="URL" placeholder={exampleLocationUrl} - helperText="Enter the full path to your entity file to start tracking your component" + helperText={t('stepInitAnalyzeUrl.urlHelperText')} margin="normal" variant="outlined" error={Boolean(errors.url)} @@ -167,7 +171,7 @@ export const StepInitAnalyzeUrl = (props: StepInitAnalyzeUrlProps) => { loading={submitted} type="submit" > - Analyze + {t('stepInitAnalyzeUrl.nextButtonText')} diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx index 45b244d582..d90f950794 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx @@ -14,8 +14,9 @@ * limitations under the License. */ +import { renderInTestApp } from '@backstage/test-utils'; import { makeStyles } from '@material-ui/core/styles'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react'; import { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; @@ -27,7 +28,7 @@ const useStyles = makeStyles({ describe('', () => { it('renders without exploding', async () => { - render( + await renderInTestApp( ', () => { it('renders card with custom styles', async () => { const { result } = renderHook(() => useStyles()); - render( + await renderInTestApp( ', () => { const title = screen.getByText('My Title'); const description = screen.getByText('description', { selector: 'strong' }); expect(title).toBeInTheDocument(); - expect(title).not.toBeVisible(); expect(description).toBeInTheDocument(); - expect(description).not.toBeVisible(); + + // FIXME: https://github.com/testing-library/jest-dom/issues/444 + // expect(title).not.toBeVisible(); + // expect(description).not.toBeVisible(); + + const card = title.closest(`.${result.current.displayNone}`); + expect(card).toBeInTheDocument(); + expect(description.closest(`.${result.current.displayNone}`)).toBe(card); }); it('renders with custom styles', async () => { const { result } = renderHook(() => useStyles()); - render( + await renderInTestApp( { const { title, description, classes } = props; + const { t } = useTranslationRef(catalogImportTranslationRef); return ( - + diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index edb2e391db..6f69b739bd 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -16,9 +16,14 @@ import { configApiRef, errorApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { TestApiProvider, mockApis } from '@backstage/test-utils'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; +import { + mockApis, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import TextField from '@material-ui/core/TextField'; -import { render, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ReactNode } from 'react'; import { AnalyzeResult, catalogImportApiRef } from '../../api'; @@ -27,7 +32,6 @@ import { generateEntities, StepPrepareCreatePullRequest, } from './StepPrepareCreatePullRequest'; -import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; describe('', () => { const catalogImportApi: jest.Mocked = { @@ -90,24 +94,23 @@ describe('', () => { it('renders without exploding', async () => { catalogApi.getEntities.mockReturnValue(Promise.resolve({ items: [] })); - render( - { - return ( - <> - - - - - - ); - }} - />, - { - wrapper: Wrapper, - }, + await renderInTestApp( + + { + return ( + <> + + + + + + ); + }} + /> + , ); const title = await screen.findByText('My title'); @@ -129,32 +132,31 @@ describe('', () => { }), ); - render( - { - return ( - <> - - - - - - ); - }} - />, - { - wrapper: Wrapper, - }, + await renderInTestApp( + + { + return ( + <> + + + + + + ); + }} + /> + , ); await userEvent.type(await screen.findByLabelText('name'), '-changed'); @@ -211,24 +213,23 @@ spec: new Error('some error'), ); - render( - { - return ( - <> - - - - - - ); - }} - />, - { - wrapper: Wrapper, - }, + await renderInTestApp( + + { + return ( + <> + + + + + + ); + }} + /> + , ); await userEvent.click( @@ -256,15 +257,14 @@ spec: }), ); - render( - , - { - wrapper: Wrapper, - }, + await renderInTestApp( + + + , ); await waitFor(() => { diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx index ce7448c0db..a40f4868e9 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -17,6 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogApiRef, humanizeEntityRef, @@ -30,8 +31,10 @@ import { ReactNode, useCallback, useEffect, useState } from 'react'; import { NestedValue, UseFormReturn } from 'react-hook-form'; import useAsync from 'react-use/esm/useAsync'; import YAML from 'yaml'; + import { AnalyzeResult, catalogImportApiRef } from '../../api'; import { useCatalogFilename } from '../../hooks'; +import { catalogImportTranslationRef } from '../../translation'; import { PartialEntity } from '../../types'; import { BackButton, NextButton } from '../Buttons'; import { PrepareResult } from '../useImportState'; @@ -127,6 +130,7 @@ export const StepPrepareCreatePullRequest = ( ) => { const { analyzeResult, onPrepare, onGoBack, renderFormFields } = props; + const { t } = useTranslationRef(catalogImportTranslationRef); const classes = useStyles(); const catalogApi = useApi(catalogApiRef); const catalogImportApi = useApi(catalogImportApiRef); @@ -252,7 +256,9 @@ export const StepPrepareCreatePullRequest = ( })} - Preview Pull Request + + {t('stepPrepareCreatePullRequest.previewPr.title')} + - Preview Entities + + {t('stepPrepareCreatePullRequest.previewCatalogInfo.title')} + - Create PR + {t('stepPrepareCreatePullRequest.nextButtonText')} diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx index 1d94b22bf4..5aa7760c98 100644 --- a/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { useTranslationRef } from '@backstage/frontend-plugin-api'; import Checkbox from '@material-ui/core/Checkbox'; import Grid from '@material-ui/core/Grid'; import ListItem from '@material-ui/core/ListItem'; @@ -21,12 +22,14 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import Typography from '@material-ui/core/Typography'; import LocationOnIcon from '@material-ui/icons/LocationOn'; +import partition from 'lodash/partition'; import { useCallback, useState } from 'react'; + import { AnalyzeResult } from '../../api'; +import { catalogImportTranslationRef } from '../../translation'; import { BackButton, NextButton } from '../Buttons'; import { EntityListComponent } from '../EntityListComponent'; import { PrepareResult } from '../useImportState'; -import partition from 'lodash/partition'; type Props = { analyzeResult: Extract; @@ -49,6 +52,8 @@ export const StepPrepareSelectLocations = ({ onPrepare, onGoBack, }: Props) => { + const { t } = useTranslationRef(catalogImportTranslationRef); + const [selectedUrls, setSelectedUrls] = useState( prepareResult?.locations.map(l => l.target) || [], ); @@ -82,8 +87,7 @@ export const StepPrepareSelectLocations = ({ {locations.length > 0 && ( <> - Select one or more locations that are present in your git - repository: + {t('stepPrepareSelectLocations.locations.description')} - + } onItemClick={onItemClick} @@ -120,7 +126,9 @@ export const StepPrepareSelectLocations = ({ {existingLocations.length > 0 && ( <> - These locations already exist in the catalog: + + {t('stepPrepareSelectLocations.existingLocations.description')} + } @@ -133,7 +141,7 @@ export const StepPrepareSelectLocations = ({ {onGoBack && } - Review + {t('stepPrepareSelectLocations.nextButtonText')} diff --git a/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx index 122c219a8b..ddf2cd51d5 100644 --- a/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx +++ b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx @@ -14,20 +14,22 @@ * limitations under the License. */ +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { Link } from '@backstage/core-components'; +import { configApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api'; +import { assertError } from '@backstage/errors'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import FormHelperText from '@material-ui/core/FormHelperText'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import LocationOnIcon from '@material-ui/icons/LocationOn'; import { useCallback, useState } from 'react'; + import { BackButton, NextButton } from '../Buttons'; import { EntityListComponent } from '../EntityListComponent'; import { PrepareResult, ReviewResult } from '../useImportState'; - -import { configApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api'; -import { Link } from '@backstage/core-components'; -import { stringifyEntityRef } from '@backstage/catalog-model'; -import { assertError } from '@backstage/errors'; +import { catalogImportTranslationRef } from '../../translation'; type Props = { prepareResult: PrepareResult; @@ -40,6 +42,7 @@ export const StepReviewLocation = ({ onReview, onGoBack, }: Props) => { + const { t } = useTranslationRef(catalogImportTranslationRef); const catalogApi = useApi(catalogApiRef); const configApi = useApi(configApiRef); const analytics = useAnalytics(); @@ -119,7 +122,7 @@ export const StepReviewLocation = ({ {prepareResult.type === 'repository' && ( <> - The following Pull Request has been opened:{' '} + {t('stepReviewLocation.prepareResult.title')} - You can already import the location and {appTitle} will fetch the - entities as soon as the Pull Request is merged. + {t('stepReviewLocation.prepareResult.description', { appTitle })} )} {exists - ? 'The following locations already exist in the catalog:' - : 'The following entities will be added to the catalog:'} + ? t('stepReviewLocation.catalog.exists') + : t('stepReviewLocation.catalog.new')} handleClick()} > - {exists ? 'Refresh' : 'Import'} + {exists + ? t('stepReviewLocation.refresh') + : t('stepReviewLocation.import')} diff --git a/plugins/catalog-import/src/translation.ts b/plugins/catalog-import/src/translation.ts index 353e09056b..857b551d9c 100644 --- a/plugins/catalog-import/src/translation.ts +++ b/plugins/catalog-import/src/translation.ts @@ -20,9 +20,132 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; export const catalogImportTranslationRef = createTranslationRef({ id: 'catalog-import', messages: { - pageTitle: 'Register an existing component', + buttons: { + back: 'Back', + }, + defaultImportPage: { + headerTitle: 'Register an existing component', + contentHeaderTitle: 'Start tracking your component in {{appTitle}}', + supportTitle: + 'Start tracking your component in {{appTitle}} by adding it to the software catalog.', + }, importInfoCard: { title: 'Register an existing component', + deepLinkTitle: 'Learn more about the Software Catalog', + linkDescription: + 'Enter the URL to your source code repository to add it to {{appTitle}}.', + fileLinkTitle: 'Link to an existing entity file', + examplePrefix: 'Example: ', + fileLinkDescription: + 'The wizard analyzes the file, previews the entities, and adds them to the {{appTitle}} catalog.', + githubIntegration: { + title: 'Link to a repository', + label: 'GitHub only', + }, + }, + importStepper: { + singleLocation: { + title: 'Select Locations', + description: 'Discovered Locations: 1', + }, + multipleLocations: { + title: 'Select Locations', + description: 'Discovered Locations: {{length, number}}', + }, + noLocation: { + title: 'Create Pull Request', + createPr: { + detailsTitle: 'Pull Request Details', + titleLabel: 'Pull Request Title', + titlePlaceholder: 'Add Backstage catalog entity descriptor files', + bodyLabel: 'Pull Request Body', + bodyPlaceholder: 'A describing text with Markdown support', + configurationTitle: 'Entity Configuration', + componentNameLabel: 'Name of the created component', + componentNamePlaceholder: 'my-component', + ownerLoadingText: 'Loading groups…', + ownerHelperText: + 'Select an owner from the list or enter a reference to a Group or a User', + ownerErrorHelperText: 'required value', + ownerLabel: 'Entity Owner', + ownerPlaceholder: 'my-group', + codeownersHelperText: + 'WARNING: This may fail if no CODEOWNERS file is found at the target location.', + }, + }, + analyze: { + title: 'Select URL', + }, + prepare: { + title: 'Import Actions', + description: 'Optional', + }, + review: { + title: 'Review', + }, + finish: { + title: 'Finish', + }, + }, + stepFinishImportLocation: { + backButtonText: 'Register another', + repository: { + title: 'The following Pull Request has been opened: ', + description: + 'Your entities will be imported as soon as the Pull Request is merged.', + }, + locations: { + new: 'The following entities have been added to the catalog:', + existing: 'A refresh was triggered for the following locations:', + viewButtonText: 'View Component', + backButtonText: 'Register another', + }, + }, + stepInitAnalyzeUrl: { + error: { + repository: "Couldn't generate entities for your repository", + locations: 'There are no entities at this location', + default: + 'Received unknown analysis result of type {{type}}. Please contact the support team.', + url: 'Must start with http:// or https://.', + }, + urlHelperText: + 'Enter the full path to your entity file to start tracking your component', + nextButtonText: 'Analyze', + }, + stepPrepareCreatePullRequest: { + previewPr: { + title: 'Preview Pull Request', + subheader: 'Create a new Pull Request', + }, + previewCatalogInfo: { + title: 'Preview Entities', + }, + nextButtonText: 'Create PR', + }, + stepPrepareSelectLocations: { + locations: { + description: + 'Select one or more locations that are present in your git repository:', + selectAll: 'Select All', + }, + existingLocations: { + description: 'These locations already exist in the catalog:', + }, + nextButtonText: 'Review', + }, + stepReviewLocation: { + prepareResult: { + title: 'The following Pull Request has been opened: ', + description: + 'You can already import the location and {{appTitle}} will fetch the entities as soon as the Pull Request is merged.', + }, + catalog: { + exists: 'The following locations already exist in the catalog:', + new: 'The following entities will be added to the catalog:', + }, + refresh: 'Refresh', + import: 'Import', }, }, }); diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 06769b29e2..2014bff9be 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -344,8 +344,8 @@ export const scaffolderReactTranslationRef: TranslationRef< readonly 'stepper.backButtonText': 'Back'; readonly 'stepper.createButtonText': 'Create'; readonly 'stepper.reviewButtonText': 'Review'; - readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; readonly 'stepper.nextButtonText': 'Next'; + readonly 'stepper.stepIndexLabel': 'Step {{index, number}}'; readonly 'templateCategoryPicker.title': 'Categories'; readonly 'templateCard.noDescription': 'No description'; readonly 'templateCard.chooseButtonText': 'Choose';