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
+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',
},
},
});