feat: home plugin support i18n

Signed-off-by: mario ma <mario.ma.node@gmail.com>
This commit is contained in:
mario ma
2025-06-04 15:55:51 +08:00
parent fbb4cf67eb
commit cef60db9c0
22 changed files with 257 additions and 43 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-home-react': patch
'@backstage/plugin-home': patch
---
Home plugin support i18n
+1
View File
@@ -44,6 +44,7 @@
"dependencies": {
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@rjsf/utils": "5.23.2"
+11
View File
@@ -9,6 +9,7 @@ import { JSX as JSX_3 } from 'react';
import { Overrides } from '@material-ui/core/styles/overrides';
import { RJSFSchema } from '@rjsf/utils';
import { StyleRules } from '@material-ui/core/styles/withStyles';
import { TranslationRef } from '@backstage/frontend-plugin-api';
import { UiSchema } from '@rjsf/utils';
// @public (undocumented)
@@ -81,6 +82,16 @@ export function createCardExtension<T>(options: {
settings?: CardSettings;
}): Extension<(props: CardExtensionProps<T>) => JSX_2.Element>;
// @public (undocumented)
export const homeReactTranslationRef: TranslationRef<
'home-react',
{
readonly 'settingsModal.title': 'Settings';
readonly 'settingsModal.closeButtonTitle': 'Close';
readonly 'cardExtension.settingsButtonTitle': 'Settings';
}
>;
// @public (undocumented)
export type PluginHomeComponentsNameToClassKey = {
PluginHomeContentModal: PluginHomeContentModalClassKey;
@@ -14,11 +14,13 @@
* limitations under the License.
*/
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import { homeReactTranslationRef } from '../translation';
/** @public */
export const SettingsModal = (props: {
@@ -28,16 +30,18 @@ export const SettingsModal = (props: {
children: JSX.Element;
}) => {
const { open, close, componentName, children } = props;
const { t } = useTranslationRef(homeReactTranslationRef);
return (
<Dialog open={open} onClose={() => close()}>
<DialogTitle>
{componentName ? `Settings - ${componentName}` : 'Settings'}
{componentName
? `${t('settingsModal.title')} - ${componentName}`
: t('settingsModal.title')}
</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button onClick={() => close()} color="primary" variant="contained">
Close
{t('settingsModal.closeButtonTitle')}
</Button>
</DialogActions>
</Dialog>
+6 -1
View File
@@ -21,6 +21,8 @@ import { InfoCard } from '@backstage/core-components';
import { SettingsModal } from './components';
import { createReactExtension, useApp } from '@backstage/core-plugin-api';
import { RJSFSchema, UiSchema } from '@rjsf/utils';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { homeReactTranslationRef } from './translation';
/**
* @public
@@ -133,6 +135,7 @@ function CardExtension<T>(props: CardExtensionComponentProps<T>) {
const app = useApp();
const { Progress } = app.getComponents();
const [settingsOpen, setSettingsOpen] = useState(false);
const { t } = useTranslationRef(homeReactTranslationRef);
if (Renderer) {
return (
@@ -158,7 +161,9 @@ function CardExtension<T>(props: CardExtensionComponentProps<T>) {
? {
action: (
<IconButton onClick={() => setSettingsOpen(true)}>
<SettingsIcon>Settings</SettingsIcon>
<SettingsIcon>
{t('cardExtension.settingsButtonTitle')}
</SettingsIcon>
</IconButton>
),
}
+1
View File
@@ -31,3 +31,4 @@ export type {
CardConfig,
} from './extensions';
export * from './overridableComponents';
export { homeReactTranslationRef } from './translation';
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright 2025 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/frontend-plugin-api';
/**
* @public
*/
export const homeReactTranslationRef = createTranslationRef({
id: 'home-react',
messages: {
settingsModal: {
title: 'Settings',
closeButtonTitle: 'Close',
},
cardExtension: {
settingsButtonTitle: 'Settings',
},
},
});
+33
View File
@@ -23,6 +23,7 @@ import { ReactNode } from 'react';
import { RendererProps as RendererProps_2 } from '@backstage/plugin-home-react';
import { RouteRef } from '@backstage/core-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
import { TranslationRef } from '@backstage/frontend-plugin-api';
import { Variant } from '@material-ui/core/styles/createTypography';
// @public
@@ -170,6 +171,38 @@ export const homePlugin: BackstagePlugin<
{}
>;
// @public (undocumented)
export const homeTranslationRef: TranslationRef<
'home',
{
readonly 'starredEntities.noStarredEntitiesMessage': 'Click the star beside an entity name to add it to this list!';
readonly 'addWidgetDialog.title': 'Add new widget to dashboard';
readonly 'customHomepageButtons.clearAll': 'Clear all';
readonly 'customHomepageButtons.edit': 'Edit';
readonly 'customHomepageButtons.restoreDefaults': 'Restore defaults';
readonly 'customHomepageButtons.addWidget': 'Add widget';
readonly 'customHomepageButtons.save': 'Save';
readonly 'customHomepage.noWidgets': "No widgets added. Start by clicking the 'Add widget' button.";
readonly 'widgetSettingsOverlay.cancelButtonTitle': 'Cancel';
readonly 'widgetSettingsOverlay.editSettingsTooptip': 'Edit settings';
readonly 'widgetSettingsOverlay.deleteWidgetTooltip': 'Delete widget';
readonly 'widgetSettingsOverlay.submitButtonTitle': 'Submit';
readonly 'starredEntityListItem.removeFavoriteEntityTitle': 'Remove entity from favorites';
readonly 'visitList.empty.title': 'There are no visits to show yet.';
readonly 'visitList.empty.description': 'Once you start using Backstage, your visits will appear here as a quick link to carry on where you left off.';
readonly 'visitList.few.title': 'The more pages you visit, the more pages will appear here.';
readonly 'quickStart.title': 'Onboarding';
readonly 'quickStart.description': 'Get started with Backstage';
readonly 'quickStart.learnMoreLinkTitle': 'Learn more';
readonly 'visitedByType.action.viewMore': 'View more';
readonly 'visitedByType.action.viewLess': 'View less';
readonly 'featuredDocsCard.empty.title': 'No documents to show';
readonly 'featuredDocsCard.empty.description': 'Create your own document. Check out our Getting Started Information';
readonly 'featuredDocsCard.empty.learnMoreLinkTitle': 'DOCS';
readonly 'featuredDocsCard.learnMoreTitle': 'LEARN MORE';
}
>;
// @public
export const isOperator: (s: string) => s is Operators;
@@ -23,6 +23,8 @@ import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import AddIcon from '@material-ui/icons/Add';
import ListItemText from '@material-ui/core/ListItemText';
import Typography from '@material-ui/core/Typography';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { homeTranslationRef } from '../../translation';
interface AddWidgetDialogProps {
widgets: Widget[];
@@ -35,9 +37,10 @@ const getTitle = (widget: Widget) => {
export const AddWidgetDialog = (props: AddWidgetDialogProps) => {
const { widgets, handleAdd } = props;
const { t } = useTranslationRef(homeTranslationRef);
return (
<>
<DialogTitle>Add new widget to dashboard</DialogTitle>
<DialogTitle>{t('addWidgetDialog.title')}</DialogTitle>
<DialogContent>
<List dense>
{widgets.map(widget => {
@@ -20,6 +20,8 @@ import DeleteIcon from '@material-ui/icons/Delete';
import AddIcon from '@material-ui/icons/Add';
import EditIcon from '@material-ui/icons/Edit';
import CancelIcon from '@material-ui/icons/Cancel';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { homeTranslationRef } from '../../translation';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -55,6 +57,7 @@ export const CustomHomepageButtons = (props: CustomHomepageButtonsProps) => {
restoreDefault,
} = props;
const styles = useStyles();
const { t } = useTranslationRef(homeTranslationRef);
return (
<>
@@ -66,7 +69,7 @@ export const CustomHomepageButtons = (props: CustomHomepageButtonsProps) => {
size="small"
startIcon={<EditIcon />}
>
Edit
{t('customHomepageButtons.edit')}
</Button>
) : (
<>
@@ -78,7 +81,7 @@ export const CustomHomepageButtons = (props: CustomHomepageButtonsProps) => {
size="small"
startIcon={<CancelIcon />}
>
Restore defaults
{t('customHomepageButtons.restoreDefaults')}
</Button>
)}
{numWidgets > 0 && (
@@ -90,7 +93,7 @@ export const CustomHomepageButtons = (props: CustomHomepageButtonsProps) => {
size="small"
startIcon={<DeleteIcon />}
>
Clear all
{t('customHomepageButtons.clearAll')}
</Button>
)}
<Button
@@ -100,7 +103,7 @@ export const CustomHomepageButtons = (props: CustomHomepageButtonsProps) => {
size="small"
startIcon={<AddIcon />}
>
Add widget
{t('customHomepageButtons.addWidget')}
</Button>
{numWidgets > 0 && (
<Button
@@ -111,7 +114,7 @@ export const CustomHomepageButtons = (props: CustomHomepageButtonsProps) => {
size="small"
startIcon={<SaveIcon />}
>
Save
{t('customHomepageButtons.save')}
</Button>
)}
</>
@@ -50,6 +50,8 @@ import {
WidgetSchema,
} from './types';
import { CardConfig } from '@backstage/plugin-home-react';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { homeTranslationRef } from '../../translation';
// eslint-disable-next-line new-cap
const ResponsiveGrid = WidthProvider(Responsive);
@@ -222,6 +224,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
const getWidgetNameFromKey = (key: string) => {
return key.split('__')[0];
};
const { t } = useTranslationRef(homeTranslationRef);
const handleAdd = (widget: Widget) => {
const widgetId = `${widget.name}__${widgets.length + 1}${Math.random()
@@ -340,7 +343,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
</Dialog>
{!editMode && widgets.length === 0 && (
<Typography variant="h5" align="center">
No widgets added. Start by clicking the 'Add widget' button.
{t('customHomepage.noWidgets')}
</Typography>
)}
<ResponsiveGrid
@@ -29,6 +29,8 @@ import { Widget } from './types';
import { withTheme } from '@rjsf/core';
import { Theme as MuiTheme } from '@rjsf/material-ui';
import validator from '@rjsf/validator-ajv8';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { homeTranslationRef } from '../../translation';
const Form = withTheme(MuiTheme);
@@ -68,6 +70,7 @@ export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => {
const styles = useStyles();
const onClose = () => setSettingsDialogOpen(false);
const { t } = useTranslationRef(homeTranslationRef);
return (
<div className={styles.settingsOverlay}>
@@ -98,10 +101,10 @@ export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => {
>
<DialogActions>
<Button color="primary" variant="contained" type="submit">
Submit
{t('widgetSettingsOverlay.submitButtonTitle')}
</Button>
<Button color="secondary" onClick={onClose}>
Cancel
{t('widgetSettingsOverlay.cancelButtonTitle')}
</Button>
</DialogActions>
</Form>
@@ -116,7 +119,7 @@ export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => {
>
{widget.settingsSchema && (
<Grid item className="overlayGridItem">
<Tooltip title="Edit settings">
<Tooltip title={t('widgetSettingsOverlay.editSettingsTooptip')}>
<IconButton
color="primary"
onClick={() => setSettingsDialogOpen(true)}
@@ -128,7 +131,7 @@ export const WidgetSettingsOverlay = (props: WidgetSettingsOverlayProps) => {
)}
{deletable !== false && (
<Grid item className="overlayGridItem">
<Tooltip title="Delete widget">
<Tooltip title={t('widgetSettingsOverlay.deleteWidgetTooltip')}>
<IconButton color="secondary" onClick={() => handleRemove(id)}>
<DeleteIcon fontSize="large" />
</IconButton>
@@ -26,6 +26,8 @@ import { entityRouteRef } from '@backstage/plugin-catalog-react';
import { useRouteRef } from '@backstage/core-plugin-api';
import { FavoriteToggle } from '@backstage/core-components';
import { makeStyles } from '@material-ui/core/styles';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { homeTranslationRef } from '../../translation';
type EntityListItemProps = {
entity: Entity;
@@ -50,6 +52,7 @@ export const StarredEntityListItem = ({
}: EntityListItemProps) => {
const classes = useStyles();
const catalogEntityRoute = useRouteRef(entityRouteRef);
const { t } = useTranslationRef(homeTranslationRef);
let secondaryText = '';
if (showKind) {
@@ -80,7 +83,7 @@ export const StarredEntityListItem = ({
>
<FavoriteToggle
id={`remove-favorite-${entity.metadata.uid}`}
title="Remove entity from favorites"
title={t('starredEntityListItem.removeFavoriteEntityTitle')}
isFavorite
onToggle={() => onToggleStarredEntity(entity)}
/>
@@ -14,16 +14,20 @@
* limitations under the License.
*/
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import Typography from '@material-ui/core/Typography';
import { homeTranslationRef } from '../../translation';
export const VisitListEmpty = () => (
<>
<Typography variant="body2" color="textSecondary">
There are no visits to show yet.
</Typography>
<Typography variant="body2" color="textSecondary">
Once you start using Backstage, your visits will appear here as a quick
link to carry on where you left off.
</Typography>
</>
);
export const VisitListEmpty = () => {
const { t } = useTranslationRef(homeTranslationRef);
return (
<>
<Typography variant="body2" color="textSecondary">
{t('visitList.empty.title')}
</Typography>
<Typography variant="body2" color="textSecondary">
{t('visitList.empty.description')}
</Typography>
</>
);
};
@@ -14,12 +14,17 @@
* limitations under the License.
*/
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import Typography from '@material-ui/core/Typography';
import { homeTranslationRef } from '../../translation';
export const VisitListFew = () => (
<>
<Typography variant="body2" color="textSecondary">
The more pages you visit, the more pages will appear here.
</Typography>
</>
);
export const VisitListFew = () => {
const { t } = useTranslationRef(homeTranslationRef);
return (
<>
<Typography variant="body2" color="textSecondary">
{t('visitList.few.title')}
</Typography>
</>
);
};
@@ -33,6 +33,8 @@ import { EntityFilterQuery } from '@backstage/catalog-client';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { homeTranslationRef } from '../../translation';
/**
* Props customizing the <FeaturedDocsCard/> component.
@@ -88,7 +90,8 @@ const useStyles = makeStyles(
export const Content = (props: FeaturedDocsCardProps): JSX.Element => {
const { emptyState, filter, linkDestination, responseLimit, subLinkText } =
props;
const linkText = subLinkText || 'LEARN MORE';
const { t } = useTranslationRef(homeTranslationRef);
const linkText = subLinkText || t('featuredDocsCard.learnMoreTitle');
const styles = useStyles();
const catalogApi = useApi(catalogApiRef);
const {
@@ -153,14 +156,14 @@ export const Content = (props: FeaturedDocsCardProps): JSX.Element => {
emptyState || (
<EmptyState
missing="data"
title="No documents to show"
description="Create your own document. Check out our Getting Started Information"
title={t('featuredDocsCard.empty.title')}
description={t('featuredDocsCard.empty.description')}
action={
<LinkButton
to="https://backstage.io/docs/features/techdocs/getting-started"
variant="contained"
>
DOCS
{t('featuredDocsCard.empty.learnMoreLinkTitle')}
</LinkButton>
}
/>
@@ -20,6 +20,8 @@ import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';
import { ContentModal } from '@backstage/plugin-home-react';
import { useStyles } from './styles';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { homeTranslationRef } from '../../translation';
/**
* Props customizing the <QuickStartCard/> component.
@@ -54,14 +56,15 @@ export type QuickStartCardProps = {
*/
export const Content = (props: QuickStartCardProps): JSX.Element => {
const styles = useStyles();
const { t } = useTranslationRef(homeTranslationRef);
return (
<>
<ContentModal
modalContent={props.image}
linkContent={props.modalTitle || 'Onboarding'}
linkContent={props.modalTitle || t('quickStart.title')}
/>
<Typography variant="body1" paragraph>
{props.cardDescription || 'Get started with Backstage'}
{props.cardDescription || t('quickStart.description')}
</Typography>
<ContentModal modalContent={props.image} linkContent={props.image} />
<Grid
@@ -78,7 +81,7 @@ export const Content = (props: QuickStartCardProps): JSX.Element => {
variant="h6"
className={styles.link}
>
{props.docsLinkTitle || 'Learn more'}
{props.docsLinkTitle || t('quickStart.learnMoreLinkTitle')}
</Link>
</Grid>
</Grid>
@@ -29,6 +29,8 @@ import { ReactNode, useState } from 'react';
import useAsync from 'react-use/esm/useAsync';
import { StarredEntityListItem } from '../../components/StarredEntityListItem/StarredEntityListItem';
import { makeStyles } from '@material-ui/core/styles';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { homeTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
tabs: {
@@ -63,6 +65,7 @@ export const Content = ({
const catalogApi = useApi(catalogApiRef);
const { starredEntities, toggleStarredEntity } = useStarredEntities();
const [activeTab, setActiveTab] = useState(0);
const { t } = useTranslationRef(homeTranslationRef);
// Grab starred entities from catalog to ensure they still exist and also retrieve display titles
const entities = useAsync(async () => {
@@ -89,7 +92,7 @@ export const Content = ({
return (
<Typography variant="body1">
{noStarredEntitiesMessage ||
'Click the star beside an entity name to add it to this list!'}
t('starredEntities.noStarredEntitiesMessage')}
</Typography>
);
@@ -17,6 +17,8 @@
import { useCallback } from 'react';
import Button from '@material-ui/core/Button';
import { useContext } from './Context';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { homeTranslationRef } from '../../translation';
export const Actions = () => {
const { collapsed, setCollapsed, visits, numVisitsOpen, loading } =
@@ -25,7 +27,10 @@ export const Actions = () => {
() => setCollapsed(prevCollapsed => !prevCollapsed),
[setCollapsed],
);
const label = collapsed ? 'View More' : 'View Less';
const { t } = useTranslationRef(homeTranslationRef);
const label = collapsed
? t('visitedByType.action.viewMore')
: t('visitedByType.action.viewLess');
if (!loading && visits.length <= numVisitsOpen) return <></>;
+1
View File
@@ -42,3 +42,4 @@ export * from './assets';
export * from './homePageComponents';
export * from './deprecated';
export * from './api';
export { homeTranslationRef } from './translation';
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright 2025 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/frontend-plugin-api';
/**
* @public
*/
export const homeTranslationRef = createTranslationRef({
id: 'home',
messages: {
addWidgetDialog: {
title: 'Add new widget to dashboard',
},
customHomepageButtons: {
edit: 'Edit',
restoreDefaults: 'Restore defaults',
clearAll: 'Clear all',
addWidget: 'Add widget',
save: 'Save',
},
customHomepage: {
noWidgets: "No widgets added. Start by clicking the 'Add widget' button.",
},
widgetSettingsOverlay: {
editSettingsTooptip: 'Edit settings',
deleteWidgetTooltip: 'Delete widget',
submitButtonTitle: 'Submit',
cancelButtonTitle: 'Cancel',
},
starredEntityListItem: {
removeFavoriteEntityTitle: 'Remove entity from favorites',
},
visitList: {
empty: {
title: 'There are no visits to show yet.',
description:
'Once you start using Backstage, your visits will appear here as a quick link to carry on where you left off.',
},
few: {
title: 'The more pages you visit, the more pages will appear here.',
},
},
quickStart: {
title: 'Onboarding',
description: 'Get started with Backstage',
learnMoreLinkTitle: 'Learn more',
},
starredEntities: {
noStarredEntitiesMessage:
'Click the star beside an entity name to add it to this list!',
},
visitedByType: {
action: {
viewMore: 'View more',
viewLess: 'View less',
},
},
featuredDocsCard: {
learnMoreTitle: 'LEARN MORE',
empty: {
title: 'No documents to show',
description:
'Create your own document. Check out our Getting Started Information',
learnMoreLinkTitle: 'DOCS',
},
},
},
});
+1
View File
@@ -6727,6 +6727,7 @@ __metadata:
"@backstage/cli": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@material-ui/core": "npm:^4.12.2"
"@material-ui/icons": "npm:^4.9.1"
"@rjsf/utils": "npm:5.23.2"