feat: add i18n support for scaffolder-react plugin

Signed-off-by: JounQin <admin@1stg.me>
This commit is contained in:
JounQin
2024-12-11 18:17:29 +08:00
parent f8ee2c76de
commit 48aab132f6
18 changed files with 257 additions and 77 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-components': patch
'@backstage/plugin-scaffolder-react': patch
---
Add i18n support for scaffolder-react plugin
+1 -1
View File
@@ -27,7 +27,7 @@
"build:all": "backstage-cli repo build --all",
"build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(adr|adr-backend|adr-common|airbrake|airbrake-backend|allure|analytics-module-ga|analytics-module-ga4|analytics-module-newrelic-browser|apache-airflow|api-docs|api-docs-module-protoc-gen-doc|apollo-explorer|app-visualizer|azure-devops|azure-devops-backend|azure-devops-common|azure-sites|azure-sites-backend|azure-sites-common|badges|badges-backend|bazaar|bazaar-backend|bitbucket-cloud-common|bitrise|catalog-graph|catalog-graphql|catalog-import|catalog-unprocessed-entities|cicd-statistics|cicd-statistics-module-gitlab|circleci|cloudbuild|code-climate|code-coverage|code-coverage-backend|codescene|config-schema|cost-insights|cost-insights-common|dynatrace|entity-feedback|entity-feedback-backend|entity-feedback-common|entity-validation|example-todo-list|example-todo-list-backend|example-todo-list-common|firehydrant|fossa|gcalendar|gcp-projects|git-release-manager|github-actions|github-deployments|github-issues|github-pull-requests-board|gitops-profiles|gocd|graphiql|graphql-backend|graphql-voyager|ilert|jenkins|jenkins-backend|jenkins-common|kafka|kafka-backend|lighthouse|lighthouse-backend|lighthouse-common|linguist|linguist-backend|linguist-common|microsoft-calendar|newrelic|newrelic-dashboard|nomad|nomad-backend|octopus-deploy|opencost|pagerduty|periskop|periskop-backend|playlist|playlist-backend|playlist-common|proxy-backend|puppetdb|rollbar|rollbar-backend|sentry|shortcuts|splunk-on-call|stack-overflow|stack-overflow-backend|stackstorm|tech-radar|tech-radar-2|todo|todo-backend|xcmetrics)'",
"build:api-reports": "yarn build:api-reports:only --tsc",
"build:api-reports:only": "NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags",
"build:api-reports:only": "LANG=en_US.UTF-8 NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --sql-reports --allow-warnings 'packages/backend-app-api,packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o ae-undocumented,ae-wrong-input-file-type --validate-release-tags",
"build:backend": "yarn workspace example-backend build",
"build:knip-reports": "backstage-repo-tools knip-reports",
"build:plugins-report": "node ./scripts/build-plugins-report",
@@ -1,19 +1,20 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
import { ExampleFetchComponent } from './ExampleFetchComponent';
describe('ExampleFetchComponent', () => {
it('renders the user table', async () => {
render(<ExampleFetchComponent />);
const { getAllByText, getByAltText, getByText, findByRole } =
await renderInTestApp(<ExampleFetchComponent />);
// Wait for the table to render
const table = await screen.findByRole('table');
const nationality = screen.getAllByText('GB');
const table = await findByRole('table');
const nationality = getAllByText('GB');
// Assert that the table contains the expected user data
expect(table).toBeInTheDocument();
expect(screen.getByAltText('Carolyn')).toBeInTheDocument();
expect(screen.getByText('Carolyn Moore')).toBeInTheDocument();
expect(screen.getByText('carolyn.moore@example.com')).toBeInTheDocument();
expect(getByAltText('Carolyn')).toBeInTheDocument();
expect(getByText('Carolyn Moore')).toBeInTheDocument();
expect(getByText('carolyn.moore@example.com')).toBeInTheDocument();
expect(nationality[0]).toBeInTheDocument();
});
});
@@ -10,7 +10,16 @@ export const coreComponentsTranslationRef: TranslationRef<
'core-components',
{
readonly 'table.filter.title': 'Filters';
readonly 'table.filter.placeholder': 'All results';
readonly 'table.filter.clearAll': 'Clear all';
readonly 'table.body.emptyDataSourceMessage': 'No records to display';
readonly 'table.toolbar.search': 'Filter';
readonly 'table.pagination.firstTooltip': 'First Page';
readonly 'table.pagination.labelDisplayedRows': '{from}-{to} of {count}';
readonly 'table.pagination.labelRowsSelect': 'rows';
readonly 'table.pagination.lastTooltip': 'Last Page';
readonly 'table.pagination.nextTooltip': 'Next Page';
readonly 'table.pagination.previousTooltip': 'Previous Page';
readonly 'signIn.title': 'Sign In';
readonly 'signIn.loginFailed': 'Login failed';
readonly 'signIn.customProvider.title': 'Custom User';
@@ -14,6 +14,10 @@
* limitations under the License.
*/
import {
TranslationFunction,
useTranslationRef,
} from '@backstage/core-plugin-api/alpha';
import MTable, {
Column,
Icons,
@@ -58,6 +62,7 @@ import React, {
useState,
} from 'react';
import { coreComponentsTranslationRef } from '../../translation';
import { SelectProps } from '../Select/Select';
import { Filter, Filters, SelectedFilters, Without } from './Filters';
import { TableLoadingBody } from './TableLoadingBody';
@@ -296,6 +301,7 @@ export function TableToolbar(toolbarProps: {
selectedFiltersLength,
toggleFilters,
} = toolbarProps;
const { t } = useTranslationRef(coreComponentsTranslationRef);
const filtersClasses = useFilterStyles();
const onSearchChanged = useCallback(
(searchText: string) => {
@@ -313,7 +319,7 @@ export function TableToolbar(toolbarProps: {
<FilterList />
</IconButton>
<Typography className={filtersClasses.title}>
Filters ({selectedFiltersLength})
{t('table.filter.title')} ({selectedFiltersLength})
</Typography>
</Box>
<StyledMTableToolbar
@@ -354,6 +360,7 @@ export function Table<T extends object = {}>(props: TableProps<T>) {
style,
...restProps
} = props;
const { t } = useTranslationRef(coreComponentsTranslationRef);
const tableClasses = useTableStyles();
const theme = useTheme();
@@ -457,7 +464,7 @@ export function Table<T extends object = {}>(props: TableProps<T>) {
<Box className={tableClasses.root}>
{filtersOpen && data && typeof data !== 'function' && filters?.length && (
<Filters
filters={constructFilters(filters, data as any[], columns)}
filters={constructFilters(filters, data as any[], columns, t)}
selectedFilters={selectedFilters}
onChangeFilters={setSelectedFilters}
/>
@@ -487,8 +494,25 @@ export function Table<T extends object = {}>(props: TableProps<T>) {
data={tableData}
style={{ width: '100%', ...style }}
localization={{
toolbar: { searchPlaceholder: 'Search', searchTooltip: 'Search' },
...localization,
body: {
emptyDataSourceMessage: t('table.body.emptyDataSourceMessage'),
...localization?.body,
},
pagination: {
firstTooltip: t('table.pagination.firstTooltip'),
labelDisplayedRows: t('table.pagination.labelDisplayedRows'),
labelRowsSelect: t('table.pagination.labelRowsSelect'),
lastTooltip: t('table.pagination.lastTooltip'),
nextTooltip: t('table.pagination.nextTooltip'),
previousTooltip: t('table.pagination.previousTooltip'),
...localization?.pagination,
},
toolbar: {
searchPlaceholder: t('table.toolbar.search'),
searchTooltip: t('table.toolbar.search'),
...localization?.toolbar,
},
}}
{...restProps}
/>
@@ -532,6 +556,7 @@ function constructFilters<T extends object>(
filterConfig: TableFilter[],
dataValue: any[] | undefined,
columns: TableColumn<T>[],
t: TranslationFunction<typeof coreComponentsTranslationRef.T>,
): Filter[] {
const extractDistinctValues = (field: string | keyof T): Set<any> => {
const distinctValues = new Set<any>();
@@ -563,7 +588,7 @@ function constructFilters<T extends object>(
filter: TableFilter,
): Without<SelectProps, 'onChange'> => {
return {
placeholder: 'All results',
placeholder: t('table.filter.placeholder'),
label: filter.column,
multiple: filter.type === 'multiple-select',
items: [...extractDistinctValues(filter.column)].sort().map(value => ({
@@ -88,6 +88,21 @@ export const coreComponentsTranslationRef = createTranslationRef({
filter: {
title: 'Filters',
clearAll: 'Clear all',
placeholder: 'All results',
},
body: {
emptyDataSourceMessage: 'No records to display',
},
pagination: {
firstTooltip: 'First Page',
labelDisplayedRows: '{from}-{to} of {count}',
labelRowsSelect: 'rows',
lastTooltip: 'Last Page',
nextTooltip: 'Next Page',
previousTooltip: 'Previous Page',
},
toolbar: {
search: 'Filter',
},
},
alertDisplay: {
@@ -42,6 +42,7 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react';
import { TemplateParameterSchema } from '@backstage/plugin-scaffolder-react';
import { TemplatePresentationV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
import { UiSchema } from '@rjsf/utils';
import { WidgetProps } from '@rjsf/utils';
import { z } from 'zod';
@@ -329,6 +330,29 @@ export type ScaffolderReactComponentsNameToClassKey = {
// @alpha (undocumented)
export type ScaffolderReactTemplateCategoryPickerClassKey = 'root' | 'label';
// @alpha (undocumented)
export const scaffolderReactTranslationRef: TranslationRef<
'scaffolder-react',
{
readonly 'workflow.noDescription': 'No description';
readonly 'passwordWidget.content': 'This widget is insecure. Please use [`ui:field: Secret`](https://backstage.io/docs/features/software-templates/writing-templates/#using-secrets) instead of `ui:widget: password`';
readonly 'scaffolderPageContextMenu.createLabel': 'Create';
readonly 'scaffolderPageContextMenu.moreLabel': 'more';
readonly 'scaffolderPageContextMenu.editorLabel': 'Manage Templates';
readonly 'scaffolderPageContextMenu.actionsLabel': 'Installed Actions';
readonly 'scaffolderPageContextMenu.tasksLabel': 'Task List';
readonly 'stepper.backButtonText': 'Back';
readonly 'stepper.createButtonText': 'Create';
readonly 'stepper.reviewButtonText': 'Review';
readonly 'stepper.stepIndexLabel': 'Step {{index, number}}';
readonly 'stepper.nextButtonText': 'Next';
readonly 'templateCategoryPicker.title': 'Categories';
readonly 'templateCard.noDescription': 'No description';
readonly 'templateCard.chooseButtonText': 'Choose';
readonly 'templateOutputs.title': 'Text Output';
}
>;
// @alpha
export const SecretWidget: (
props: Pick<
+2
View File
@@ -15,3 +15,5 @@
*/
export * from './next';
export { scaffolderReactTranslationRef } from './translation';
@@ -14,15 +14,20 @@
* limitations under the License.
*/
import { WidgetProps } from '@rjsf/utils';
import TextField from '@material-ui/core/TextField';
import React from 'react';
import FormHelperText from '@material-ui/core/FormHelperText';
import { MarkdownContent } from '@backstage/core-components';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import FormHelperText from '@material-ui/core/FormHelperText';
import TextField from '@material-ui/core/TextField';
import { WidgetProps } from '@rjsf/utils';
import React from 'react';
import { scaffolderReactTranslationRef } from '../../../translation';
export const PasswordWidget = (
props: Pick<WidgetProps, 'onChange' | 'schema' | 'value'>,
) => {
const { t } = useTranslationRef(scaffolderReactTranslationRef);
const {
value,
onChange,
@@ -42,10 +47,7 @@ export const PasswordWidget = (
autoComplete="off"
/>
<FormHelperText error>
<MarkdownContent
content="This widget is insecure. Please use [`ui:field: Secret`](https://backstage.io/docs/features/software-templates/writing-templates/#using-secrets) instead of
`ui:widget: password`"
/>
<MarkdownContent content={t('passwordWidget.content')} />
</FormHelperText>
</>
);
@@ -14,14 +14,15 @@
* limitations under the License.
*/
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import IconButton from '@material-ui/core/IconButton';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import MenuItem from '@material-ui/core/MenuItem';
import MenuList from '@material-ui/core/MenuList';
import Popover from '@material-ui/core/Popover';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import { makeStyles } from '@material-ui/core/styles';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import Description from '@material-ui/icons/Description';
import Edit from '@material-ui/icons/Edit';
import List from '@material-ui/icons/List';
@@ -31,6 +32,8 @@ import { usePermission } from '@backstage/plugin-permission-react';
import { taskReadPermission } from '@backstage/plugin-scaffolder-common/alpha';
import { templateManagementPermission } from '@backstage/plugin-scaffolder-common/alpha';
import { scaffolderReactTranslationRef } from '../../../translation';
const useStyles = makeStyles(theme => ({
button: {
color: theme.page.fontColor,
@@ -53,6 +56,7 @@ export type ScaffolderPageContextMenuProps = {
export function ScaffolderPageContextMenu(
props: ScaffolderPageContextMenuProps,
) {
const { t } = useTranslationRef(scaffolderReactTranslationRef);
const { onEditorClicked, onActionsClicked, onTasksClicked, onCreateClicked } =
props;
const classes = useStyles();
@@ -87,7 +91,7 @@ export function ScaffolderPageContextMenu(
<>
<IconButton
id="long-menu"
aria-label="more"
aria-label={t('scaffolderPageContextMenu.moreLabel')}
aria-controls="long-menu"
aria-expanded={!!anchorEl}
aria-haspopup="true"
@@ -113,7 +117,9 @@ export function ScaffolderPageContextMenu(
<ListItemIcon>
<CreateComponentIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Create" />
<ListItemText
primary={t('scaffolderPageContextMenu.createLabel')}
/>
</MenuItem>
)}
{onEditorClicked && canManageTemplates && (
@@ -121,7 +127,9 @@ export function ScaffolderPageContextMenu(
<ListItemIcon>
<Edit fontSize="small" />
</ListItemIcon>
<ListItemText primary="Manage Templates" />
<ListItemText
primary={t('scaffolderPageContextMenu.editorLabel')}
/>
</MenuItem>
)}
{onActionsClicked && (
@@ -129,7 +137,9 @@ export function ScaffolderPageContextMenu(
<ListItemIcon>
<Description fontSize="small" />
</ListItemIcon>
<ListItemText primary="Installed Actions" />
<ListItemText
primary={t('scaffolderPageContextMenu.actionsLabel')}
/>
</MenuItem>
)}
{onTasksClicked && canReadTasks && (
@@ -137,7 +147,9 @@ export function ScaffolderPageContextMenu(
<ListItemIcon>
<List fontSize="small" />
</ListItemIcon>
<ListItemText primary="Task List" />
<ListItemText
primary={t('scaffolderPageContextMenu.tasksLabel')}
/>
</MenuItem>
)}
</MenuList>
@@ -14,44 +14,47 @@
* limitations under the License.
*/
import { useAnalytics, useApiHolder } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import {
FieldExtensionOptions,
FormProps,
LayoutOptions,
ReviewStepProps,
TemplateParameterSchema,
} from '@backstage/plugin-scaffolder-react';
import { JsonValue } from '@backstage/types';
import MuiStepper from '@material-ui/core/Stepper';
import MuiStep from '@material-ui/core/Step';
import MuiStepLabel from '@material-ui/core/StepLabel';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import MuiStep from '@material-ui/core/Step';
import MuiStepLabel from '@material-ui/core/StepLabel';
import MuiStepper from '@material-ui/core/Stepper';
import { makeStyles } from '@material-ui/core/styles';
import { type IChangeEvent } from '@rjsf/core';
import { ErrorSchema } from '@rjsf/utils';
import { customizeValidator } from '@rjsf/validator-ajv8';
import ajvErrors from 'ajv-errors';
import { merge } from 'lodash';
import React, {
ComponentType,
useCallback,
useMemo,
useState,
type ReactNode,
ComponentType,
} from 'react';
import { scaffolderReactTranslationRef } from '../../../translation';
import { useFormDataFromQuery, useTemplateSchema } from '../../hooks';
import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps';
import { Form } from '../Form';
import { PasswordWidget } from '../PasswordWidget/PasswordWidget';
import { ReviewState, type ReviewStateProps } from '../ReviewState';
import {
createAsyncValidators,
type FormValidation,
} from './createAsyncValidators';
import { ReviewState, type ReviewStateProps } from '../ReviewState';
import { useTemplateSchema, useFormDataFromQuery } from '../../hooks';
import { customizeValidator } from '@rjsf/validator-ajv8';
import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps';
import { hasErrors } from './utils';
import * as FieldOverrides from './FieldOverrides';
import { Form } from '../Form';
import {
TemplateParameterSchema,
LayoutOptions,
FieldExtensionOptions,
FormProps,
} from '@backstage/plugin-scaffolder-react';
import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
import { ErrorListTemplate } from './ErrorListTemplate';
import { makeStyles } from '@material-ui/core/styles';
import { PasswordWidget } from '../PasswordWidget/PasswordWidget';
import ajvErrors from 'ajv-errors';
import { merge } from 'lodash';
import * as FieldOverrides from './FieldOverrides';
import { hasErrors } from './utils';
const validator = customizeValidator();
ajvErrors(validator.ajv);
@@ -111,13 +114,14 @@ export type StepperProps = {
* @alpha
*/
export const Stepper = (stepperProps: StepperProps) => {
const { t } = useTranslationRef(scaffolderReactTranslationRef);
const { layouts = [], components = {}, ...props } = stepperProps;
const {
ReviewStateComponent = ReviewState,
ReviewStepComponent,
backButtonText = 'Back',
createButtonText = 'Create',
reviewButtonText = 'Review',
backButtonText = t('stepper.backButtonText'),
createButtonText = t('stepper.createButtonText'),
reviewButtonText = t('stepper.reviewButtonText'),
} = components;
const analytics = useAnalytics();
const { presentation, steps } = useTemplateSchema(props.manifest);
@@ -235,7 +239,7 @@ export const Stepper = (stepperProps: StepperProps) => {
return (
<MuiStep key={index}>
<MuiStepLabel
aria-label={`Step ${index + 1}`}
aria-label={t('stepper.stepIndexLabel', { index: index + 1 })}
style={{ cursor: isAllowedLabelClick ? 'pointer' : 'default' }}
onClick={() => {
if (isAllowedLabelClick) setActiveStep(index);
@@ -286,7 +290,9 @@ export const Stepper = (stepperProps: StepperProps) => {
type="submit"
disabled={isValidating}
>
{activeStep === steps.length - 1 ? reviewLabel : 'Next'}
{activeStep === steps.length - 1
? reviewLabel
: t('stepper.nextButtonText')}
</Button>
</div>
</Form>
@@ -15,11 +15,14 @@
*/
import { UserIcon } from '@backstage/core-components';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { EntityRefLinks } from '@backstage/plugin-catalog-react';
import Button from '@material-ui/core/Button';
import { makeStyles, Theme } from '@material-ui/core/styles';
import React from 'react';
import { scaffolderReactTranslationRef } from '../../../translation';
const useStyles = makeStyles<Theme>(theme => ({
footer: {
display: 'flex',
@@ -50,6 +53,7 @@ export const TemplateCardActions = ({
handleChoose,
ownedByRelations,
}: TemplateCardActionsProps) => {
const { t } = useTranslationRef(scaffolderReactTranslationRef);
const styles = useStyles();
return (
@@ -78,7 +82,7 @@ export const TemplateCardActions = ({
data-testid="template-card-actions--create"
onClick={handleChoose}
>
Choose
{t('templateCard.chooseButtonText')}
</Button>
) : null}
</div>
@@ -15,12 +15,15 @@
*/
import { MarkdownContent } from '@backstage/core-components';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import type { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import Grid from '@material-ui/core/Grid';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import { scaffolderReactTranslationRef } from '../../../translation';
const useStyles = makeStyles(() => ({
box: {
overflow: 'hidden',
@@ -45,13 +48,16 @@ export interface TemplateCardContentProps {
template: TemplateEntityV1beta3;
}
export const TemplateCardContent = ({ template }: TemplateCardContentProps) => {
const { t } = useTranslationRef(scaffolderReactTranslationRef);
const styles = useStyles();
return (
<Grid item xs={12} data-testid="template-card-content-grid">
<Box className={styles.box} data-testid="template-card-content-container">
<MarkdownContent
className={styles.markdown}
content={template.metadata.description ?? 'No description'}
content={
template.metadata.description ?? t('templateCard.noDescription')
}
/>
</Box>
</Grid>
@@ -14,9 +14,10 @@
* limitations under the License.
*/
import React, { ReactNode } from 'react';
import capitalize from 'lodash/capitalize';
import { Progress } from '@backstage/core-components';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { useEntityTypeFilter } from '@backstage/plugin-catalog-react';
import Box from '@material-ui/core/Box';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
@@ -27,8 +28,10 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox';
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { useEntityTypeFilter } from '@backstage/plugin-catalog-react';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import capitalize from 'lodash/capitalize';
import React, { ReactNode } from 'react';
import { scaffolderReactTranslationRef } from '../../../translation';
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
@@ -50,6 +53,7 @@ const useStyles = makeStyles(
* @alpha
*/
export const TemplateCategoryPicker = () => {
const { t } = useTranslationRef(scaffolderReactTranslationRef);
const classes = useStyles();
const alertApi = useApi(alertApiRef);
const { error, loading, availableTypes, selectedTypes, setSelectedTypes } =
@@ -75,7 +79,7 @@ export const TemplateCategoryPicker = () => {
component="label"
htmlFor="categories-picker"
>
Categories
{t('templateCategoryPicker.title')}
</Typography>
<Autocomplete<string, true>
PopperComponent={popperProps => (
@@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useCallback } from 'react';
import { Link, Progress } from '@backstage/core-components';
import { errorApiRef, IconComponent, useApi } from '@backstage/core-plugin-api';
import { useEntityList } from '@backstage/plugin-catalog-react';
import {
isTemplateEntityV1beta3,
TemplateEntityV1beta3,
} from '@backstage/plugin-scaffolder-common';
import { Progress, Link } from '@backstage/core-components';
import Typography from '@material-ui/core/Typography';
import { errorApiRef, IconComponent, useApi } from '@backstage/core-plugin-api';
import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react';
import Typography from '@material-ui/core/Typography';
import React, { useCallback } from 'react';
import { TemplateGroup } from '../TemplateGroup/TemplateGroup';
/**
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { InfoCard, MarkdownContent } from '@backstage/core-components';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import {
ScaffolderOutputText,
ScaffolderTaskOutput,
@@ -21,6 +22,8 @@ import {
import Box from '@material-ui/core/Box';
import Paper from '@material-ui/core/Paper';
import React, { useEffect, useMemo, useState } from 'react';
import { scaffolderReactTranslationRef } from '../../../translation';
import { LinkOutputs } from './LinkOutputs';
import { TextOutputs } from './TextOutputs';
@@ -32,6 +35,7 @@ import { TextOutputs } from './TextOutputs';
export const DefaultTemplateOutputs = (props: {
output?: ScaffolderTaskOutput;
}) => {
const { t } = useTranslationRef(scaffolderReactTranslationRef);
const { output } = props;
const [textOutputIndex, setTextOutputIndex] = useState<number | undefined>(
undefined,
@@ -40,7 +44,7 @@ export const DefaultTemplateOutputs = (props: {
useEffect(() => {
if (textOutputIndex === undefined && output?.text) {
const defaultIndex = output.text.findIndex(
(t: ScaffolderOutputText) => t.default,
(text: ScaffolderOutputText) => text.default,
);
setTextOutputIndex(defaultIndex >= 0 ? defaultIndex : 0);
}
@@ -87,7 +91,7 @@ export const DefaultTemplateOutputs = (props: {
{textOutput ? (
<Box paddingBottom={2} data-testid="text-output-box">
<InfoCard
title={textOutput.title ?? 'Text Output'}
title={textOutput.title ?? t('templateOutputs.title')}
noPadding
titleTypographyProps={{ component: 'h2' }}
>
@@ -14,23 +14,26 @@
* limitations under the License.
*/
import React, { useCallback, useEffect } from 'react';
import { stringifyEntityRef } from '@backstage/catalog-model';
import {
Content,
InfoCard,
MarkdownContent,
Progress,
} from '@backstage/core-components';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { makeStyles } from '@material-ui/core/styles';
import { errorApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api';
import { useTemplateParameterSchema } from '../../hooks/useTemplateParameterSchema';
import { Stepper, type StepperProps } from '../Stepper/Stepper';
import { SecretsContextProvider } from '../../../secrets/SecretsContext';
import { useFilteredSchemaProperties } from '../../hooks/useFilteredSchemaProperties';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { ReviewStepProps } from '@backstage/plugin-scaffolder-react';
import { useTemplateTimeSavedMinutes } from '../../hooks/useTemplateTimeSaved';
import { JsonValue } from '@backstage/types';
import { makeStyles } from '@material-ui/core/styles';
import React, { useCallback, useEffect } from 'react';
import { SecretsContextProvider } from '../../../secrets/SecretsContext';
import { scaffolderReactTranslationRef } from '../../../translation';
import { useFilteredSchemaProperties } from '../../hooks/useFilteredSchemaProperties';
import { useTemplateParameterSchema } from '../../hooks/useTemplateParameterSchema';
import { useTemplateTimeSavedMinutes } from '../../hooks/useTemplateTimeSaved';
import { Stepper, type StepperProps } from '../Stepper/Stepper';
const useStyles = makeStyles({
markdown: {
@@ -70,6 +73,7 @@ export type WorkflowProps = {
* @alpha
*/
export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => {
const { t } = useTranslationRef(scaffolderReactTranslationRef);
const { title, description, namespace, templateName, onCreate, ...props } =
workflowProps;
@@ -123,7 +127,9 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => {
className={styles.markdown}
linkTarget="_blank"
content={
description ?? sortedManifest.description ?? 'No description'
description ??
sortedManifest.description ??
t('workflow.noDescription')
}
/>
}
@@ -0,0 +1,54 @@
/*
* 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 scaffolderReactTranslationRef = createTranslationRef({
id: 'scaffolder-react',
messages: {
passwordWidget: {
content:
'This widget is insecure. Please use [`ui:field: Secret`](https://backstage.io/docs/features/software-templates/writing-templates/#using-secrets) instead of `ui:widget: password`',
},
scaffolderPageContextMenu: {
moreLabel: 'more',
createLabel: 'Create',
editorLabel: 'Manage Templates',
actionsLabel: 'Installed Actions',
tasksLabel: 'Task List',
},
stepper: {
backButtonText: 'Back',
createButtonText: 'Create',
reviewButtonText: 'Review',
stepIndexLabel: 'Step {{index, number}}',
nextButtonText: 'Next',
},
templateCategoryPicker: {
title: 'Categories',
},
templateCard: {
noDescription: 'No description',
chooseButtonText: 'Choose',
},
templateOutputs: {
title: 'Text Output',
},
workflow: {
noDescription: 'No description',
},
},
});