feat: org plugin support i18n

Signed-off-by: mario ma <mario.ma.node@gmail.com>
This commit is contained in:
mario ma
2025-05-28 19:26:24 +08:00
parent f92e794480
commit 4fe364fa07
8 changed files with 199 additions and 41 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
Org plugin support i18n
+34
View File
@@ -13,6 +13,7 @@ import { ExtensionInput } from '@backstage/frontend-plugin-api';
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react';
import { TranslationRef } from '@backstage/frontend-plugin-api';
// @alpha (undocumented)
const _default: FrontendPlugin<
@@ -203,5 +204,38 @@ const _default: FrontendPlugin<
>;
export default _default;
// @alpha (undocumented)
export const orgTranslationRef: TranslationRef<
'org',
{
readonly 'groupProfileCard.groupNotFound': 'Group not found';
readonly 'groupProfileCard.editIconButtonTitle': 'Edit Metadata';
readonly 'groupProfileCard.refreshIconButtonTitle': 'Schedule entity refresh';
readonly 'groupProfileCard.refreshIconButtonAriaLabel': 'Refresh';
readonly 'groupProfileCard.listItemTitle.email': 'Email';
readonly 'groupProfileCard.listItemTitle.entityRef': 'Entity Ref';
readonly 'groupProfileCard.listItemTitle.parentGroup': 'Parent Group';
readonly 'groupProfileCard.listItemTitle.childGroups': 'Child Groups';
readonly 'membersListCard.title': 'Members';
readonly 'membersListCard.subtitle': 'of {{groupName}}';
readonly 'membersListCard.paginationLabel': ', page {{page}} of {{nbPages}}';
readonly 'membersListCard.noMembersDescription': 'This group has no members.';
readonly 'membersListCard.aggregateMembersToggle.ariaLabel': 'Users Type Switch';
readonly 'membersListCard.aggregateMembersToggle.directMembers': 'Direct Members';
readonly 'membersListCard.aggregateMembersToggle.aggregatedMembers': 'Aggregated Members';
readonly 'ownershipCard.title': 'Ownership';
readonly 'ownershipCard.aggregateRelationsToggle.ariaLabel': 'Ownership Type Switch';
readonly 'ownershipCard.aggregateRelationsToggle.directRelations': 'Direct Relations';
readonly 'ownershipCard.aggregateRelationsToggle.aggregatedRelations': 'Aggregated Relations';
readonly 'userProfileCard.editIconButtonTitle': 'Edit Metadata';
readonly 'userProfileCard.listItemTitle.email': 'Email';
readonly 'userProfileCard.listItemTitle.memberOf': 'Member of';
readonly 'userProfileCard.userNotFound': 'User not found';
readonly 'userProfileCard.moreGroupButtonTitle': '...More ({{number}})';
readonly 'userProfileCard.allGroupDialog.title': "All {{name}}'s groups:";
readonly 'userProfileCard.allGroupDialog.closeButtonTitle': 'Close';
}
>;
// (No @packageDocumentation comment for this package)
```
+2
View File
@@ -98,3 +98,5 @@ export default createFrontendPlugin({
catalogIndex: catalogIndexRouteRef,
}),
});
export { orgTranslationRef } from './translation';
@@ -55,6 +55,8 @@ import PermIdentityIcon from '@material-ui/icons/PermIdentity';
import { LinksGroup } from '../../Meta';
import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha';
import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { orgTranslationRef } from '../../../../translation';
const CardTitle = (props: { title: string }) => (
<Box display="flex" alignItems="center">
@@ -74,6 +76,7 @@ export const GroupProfileCard = (props: {
const { allowed: canRefresh } = useEntityPermission(
catalogEntityRefreshPermission,
);
const { t } = useTranslationRef(orgTranslationRef);
const refreshEntity = useCallback(async () => {
await catalogApi.refreshEntity(stringifyEntityRef(group));
@@ -85,7 +88,9 @@ export const GroupProfileCard = (props: {
}, [catalogApi, alertApi, group]);
if (!group) {
return <Alert severity="error">Group not found</Alert>;
return (
<Alert severity="error">{t('groupProfileCard.groupNotFound')}</Alert>
);
}
const {
@@ -111,15 +116,19 @@ export const GroupProfileCard = (props: {
const emailHref = profile?.email ? `mailto:${profile.email}` : '#';
const infoCardAction = entityMetadataEditUrl ? (
<IconButton
aria-label="Edit"
title="Edit Metadata"
aria-label={t('groupProfileCard.editIconButtonTitle')}
title={t('groupProfileCard.editIconButtonTitle')}
component={Link}
to={entityMetadataEditUrl}
>
<EditIcon />
</IconButton>
) : (
<IconButton aria-label="Edit" disabled title="Edit Metadata">
<IconButton
aria-label={t('groupProfileCard.editIconButtonTitle')}
disabled
title={t('groupProfileCard.editIconButtonTitle')}
>
<EditIcon />
</IconButton>
);
@@ -133,8 +142,8 @@ export const GroupProfileCard = (props: {
<>
{allowRefresh && canRefresh && (
<IconButton
aria-label="Refresh"
title="Schedule entity refresh"
aria-label={t('groupProfileCard.refreshIconButtonAriaLabel')}
title={t('groupProfileCard.refreshIconButtonTitle')}
onClick={refreshEntity}
>
<CachedIcon />
@@ -152,31 +161,33 @@ export const GroupProfileCard = (props: {
<List>
<ListItem>
<ListItemIcon>
<Tooltip title="Entity Ref">
<Tooltip title={t('groupProfileCard.listItemTitle.entityRef')}>
<PermIdentityIcon />
</Tooltip>
</ListItemIcon>
<ListItemText
primary={stringifyEntityRef(group)}
secondary="Entity Ref"
secondary={t('groupProfileCard.listItemTitle.entityRef')}
/>
</ListItem>
{profile?.email && (
<ListItem>
<ListItemIcon>
<Tooltip title="Email">
<Tooltip title={t('groupProfileCard.listItemTitle.email')}>
<EmailIcon />
</Tooltip>
</ListItemIcon>
<ListItemText
primary={<Link to={emailHref}>{profile.email}</Link>}
secondary="Email"
secondary={t('groupProfileCard.listItemTitle.email')}
/>
</ListItem>
)}
<ListItem>
<ListItemIcon>
<Tooltip title="Parent Group">
<Tooltip
title={t('groupProfileCard.listItemTitle.parentGroup')}
>
<AccountTreeIcon />
</Tooltip>
</ListItemIcon>
@@ -191,12 +202,14 @@ export const GroupProfileCard = (props: {
'N/A'
)
}
secondary="Parent Group"
secondary={t('groupProfileCard.listItemTitle.parentGroup')}
/>
</ListItem>
<ListItem>
<ListItemIcon>
<Tooltip title="Child Groups">
<Tooltip
title={t('groupProfileCard.listItemTitle.childGroups')}
>
<GroupIcon />
</Tooltip>
</ListItemIcon>
@@ -211,7 +224,7 @@ export const GroupProfileCard = (props: {
'N/A'
)
}
secondary="Child Groups"
secondary={t('groupProfileCard.listItemTitle.childGroups')}
/>
</ListItem>
{props?.showLinks && <LinksGroup links={links} />}
@@ -48,6 +48,8 @@ import {
removeDuplicateEntitiesFrom,
} from '../../../../helpers/helpers';
import { EntityRelationAggregation } from '../../types';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { orgTranslationRef } from '../../../../translation';
/** @public */
export type MemberComponentClassKey = 'card' | 'avatar';
@@ -157,8 +159,9 @@ export const MembersListCard = (props: {
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
}) => {
const { t } = useTranslationRef(orgTranslationRef);
const {
memberDisplayTitle = 'Members',
memberDisplayTitle = t('membersListCard.title'),
pageSize = 50,
showAggregateMembersToggle,
relationType = 'memberof',
@@ -236,7 +239,13 @@ export const MembersListCard = (props: {
}
const nbPages = Math.ceil((members?.length || 0) / pageSize);
const paginationLabel = nbPages < 2 ? '' : `, page ${page} of ${nbPages}`;
const paginationLabel =
nbPages < 2
? ''
: t('membersListCard.paginationLabel', {
page: String(page),
nbPages: String(nbPages),
});
const pagination = (
<Pagination
@@ -260,9 +269,7 @@ export const MembersListCard = (props: {
} else {
memberList = (
<Box p={2}>
<Typography>
This group has no {memberDisplayTitle.toLocaleLowerCase()}.
</Typography>
<Typography>{t('membersListCard.noMembersDescription')}</Typography>
</Box>
);
}
@@ -273,23 +280,29 @@ export const MembersListCard = (props: {
title={`${memberDisplayTitle} (${
members?.length || 0
}${paginationLabel})`}
subheader={`of ${displayName}`}
subheader={t('membersListCard.subtitle', {
groupName: displayName,
})}
{...(nbPages <= 1 ? {} : { actions: pagination })}
className={classes.root}
cardClassName={classes.cardContent}
>
{showAggregateMembersToggle && (
<>
Direct Members
{t('membersListCard.aggregateMembersToggle.directMembers')}
<Switch
color="primary"
checked={showAggregateMembers}
onChange={() => {
setShowAggregateMembers(!showAggregateMembers);
}}
inputProps={{ 'aria-label': 'Users Type Switch' }}
inputProps={{
'aria-label': t(
'membersListCard.aggregateMembersToggle.ariaLabel',
),
}}
/>
Aggregated Members
{t('membersListCard.aggregateMembersToggle.aggregatedMembers')}
</>
)}
{showAggregateMembers && loadingDescendantMembers ? (
@@ -26,6 +26,8 @@ import { makeStyles } from '@material-ui/core/styles';
import { useEffect, useState } from 'react';
import { ComponentsGrid } from './ComponentsGrid';
import { EntityRelationAggregation } from '../types';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { orgTranslationRef } from '../../../translation';
const useStyles = makeStyles(theme => ({
card: {
@@ -83,6 +85,7 @@ export const OwnershipCard = (props: {
hideRelationsToggle === undefined ? false : hideRelationsToggle;
const classes = useStyles();
const { entity } = useEntity();
const { t } = useTranslationRef(orgTranslationRef);
const defaultRelationAggregation =
entity.kind === 'User' ? 'aggregated' : 'direct';
@@ -98,7 +101,7 @@ export const OwnershipCard = (props: {
return (
<InfoCard
title="Ownership"
title={t('ownershipCard.title')}
variant={variant}
className={classes.card}
cardClassName={classes.cardContent}
@@ -110,13 +113,19 @@ export const OwnershipCard = (props: {
<ListItemSecondaryAction
className={classes.listItemSecondaryAction}
>
Direct Relations
{t('ownershipCard.aggregateRelationsToggle.directRelations')}
<Tooltip
placement="top"
arrow
title={`${
getRelationAggregation === 'direct' ? 'Direct' : 'Aggregated'
} Relations`}
title={
getRelationAggregation === 'direct'
? t(
'ownershipCard.aggregateRelationsToggle.directRelations',
)
: t(
'ownershipCard.aggregateRelationsToggle.aggregatedRelations',
)
}
>
<Switch
color="primary"
@@ -129,10 +138,14 @@ export const OwnershipCard = (props: {
setRelationAggregation(updatedRelationAggregation);
}}
name="pin"
inputProps={{ 'aria-label': 'Ownership Type Switch' }}
inputProps={{
'aria-label': t(
'ownershipCard.aggregateRelationsToggle.ariaLabel',
),
}}
/>
</Tooltip>
Aggregated Relations
{t('ownershipCard.aggregateRelationsToggle.aggregatedRelations')}
</ListItemSecondaryAction>
</ListItem>
</List>
@@ -55,6 +55,8 @@ import { LinksGroup } from '../../Meta';
import PersonIcon from '@material-ui/icons/Person';
import { useCallback, useState } from 'react';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { orgTranslationRef } from '../../../../translation';
const useStyles = makeStyles((theme: any) => ({
closeButton: {
@@ -92,6 +94,7 @@ export const UserProfileCard = (props: {
const classes = useStyles();
const { entity: user } = useEntity<UserEntity>();
const [isAllGroupsDialogOpen, setIsAllGroupsDialogOpen] = useState(false);
const { t } = useTranslationRef(orgTranslationRef);
const toggleAllGroupsDialog = useCallback(
() =>
@@ -102,7 +105,7 @@ export const UserProfileCard = (props: {
);
if (!user) {
return <Alert severity="error">User not found</Alert>;
return <Alert severity="error">{t('userProfileCard.userNotFound')}</Alert>;
}
const entityMetadataEditUrl =
@@ -127,8 +130,8 @@ export const UserProfileCard = (props: {
<>
{entityMetadataEditUrl && (
<IconButton
aria-label="Edit"
title="Edit Metadata"
aria-label={t('userProfileCard.editIconButtonTitle')}
title={t('userProfileCard.editIconButtonTitle')}
component={Link}
to={entityMetadataEditUrl}
>
@@ -148,7 +151,7 @@ export const UserProfileCard = (props: {
{profile?.email && (
<ListItem>
<ListItemIcon>
<Tooltip title="Email">
<Tooltip title={t('userProfileCard.listItemTitle.email')}>
<EmailIcon />
</Tooltip>
</ListItemIcon>
@@ -161,7 +164,7 @@ export const UserProfileCard = (props: {
{maxRelations === undefined || maxRelations > 0 ? (
<ListItem>
<ListItemIcon>
<Tooltip title="Member of">
<Tooltip title={t('userProfileCard.listItemTitle.memberOf')}>
<GroupIcon />
</Tooltip>
</ListItemIcon>
@@ -179,9 +182,11 @@ export const UserProfileCard = (props: {
onClick={toggleAllGroupsDialog}
disableRipple
>
{` ...More (${
memberOfRelations.length - maxRelations
})`}
{t('userProfileCard.moreGroupButtonTitle', {
number: String(
memberOfRelations.length - maxRelations,
),
})}
</BaseButton>
</>
) : null}
@@ -203,10 +208,12 @@ export const UserProfileCard = (props: {
fullWidth
>
<DialogTitle id="view-all-groups-dialog-title">
All {user.metadata.name}'s groups:
{t('userProfileCard.allGroupDialog.title', {
name: user.metadata.name,
})}
<IconButton
className={classes.closeButton}
aria-label="close"
aria-label={t('userProfileCard.allGroupDialog.closeButtonTitle')}
onClick={toggleAllGroupsDialog}
>
<CloseIcon />
@@ -216,7 +223,9 @@ export const UserProfileCard = (props: {
<EntityRefLinks entityRefs={memberOfRelations} defaultKind="Group" />
</DialogContent>
<DialogActions>
<Button onClick={toggleAllGroupsDialog}>Close</Button>
<Button onClick={toggleAllGroupsDialog}>
{t('userProfileCard.allGroupDialog.closeButtonTitle')}
</Button>
</DialogActions>
</Dialog>
</InfoCard>
+69
View File
@@ -0,0 +1,69 @@
/*
* 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';
/**
* @alpha
*/
export const orgTranslationRef = createTranslationRef({
id: 'org',
messages: {
groupProfileCard: {
groupNotFound: 'Group not found',
editIconButtonTitle: 'Edit Metadata',
refreshIconButtonTitle: 'Schedule entity refresh',
refreshIconButtonAriaLabel: 'Refresh',
listItemTitle: {
entityRef: 'Entity Ref',
email: 'Email',
parentGroup: 'Parent Group',
childGroups: 'Child Groups',
},
},
membersListCard: {
title: 'Members',
subtitle: 'of {{groupName}}',
paginationLabel: ', page {{page}} of {{nbPages}}',
noMembersDescription: 'This group has no members.',
aggregateMembersToggle: {
directMembers: 'Direct Members',
aggregatedMembers: 'Aggregated Members',
ariaLabel: 'Users Type Switch',
},
},
ownershipCard: {
title: 'Ownership',
aggregateRelationsToggle: {
directRelations: 'Direct Relations',
aggregatedRelations: 'Aggregated Relations',
ariaLabel: 'Ownership Type Switch',
},
},
userProfileCard: {
userNotFound: 'User not found',
editIconButtonTitle: 'Edit Metadata',
listItemTitle: {
email: 'Email',
memberOf: 'Member of',
},
moreGroupButtonTitle: '...More ({{number}})',
allGroupDialog: {
title: "All {{name}}'s groups:",
closeButtonTitle: 'Close',
},
},
},
});