fix(i18n): wrap untranslated frontend strings and add i18n lint rule (#37776)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-02-23 00:27:37 -05:00
committed by GitHub
parent 672a380587
commit 3f64ad3da5
71 changed files with 65097 additions and 15158 deletions

View File

@@ -288,11 +288,11 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
{schema && <Breadcrumb.Item>{schema}</Breadcrumb.Item>}
<Breadcrumb.Item> </Breadcrumb.Item>
</Breadcrumb>
<div style={{ display: 'none' }}>
<div style={{ display: 'none' }} aria-hidden="true">
<CopyToClipboard
copyNode={
<button type="button" ref={copyStatementActionRef}>
invisible button
{t('Copy')}
</button>
}
text={tableData.selectStar}
@@ -305,7 +305,7 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
title={t('CREATE VIEW statement')}
triggerNode={
<button type="button" ref={showViewStatementActionRef}>
invisible button
{t('Show SQL')}
</button>
}
/>

View File

@@ -375,7 +375,7 @@ export default class CRUDCollection extends PureComponent<
`}
>
<Icons.DeleteOutlined
aria-label="Delete item"
aria-label={t('Delete item')}
className="pointer"
data-test="crud-delete-icon"
role="button"

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import { Icons } from '@superset-ui/core/components';
import { t } from '@apache-superset/core';
import { useTheme } from '@apache-superset/core/ui';
interface IssueCodeProps {
@@ -33,7 +34,7 @@ export function IssueCode({ code, message }: IssueCodeProps) {
href={`https://superset.apache.org/docs/using-superset/issue-codes#issue-${code}`}
rel="noopener noreferrer"
target="_blank"
aria-label="Superset docs link"
aria-label={t('Superset docs link')}
>
<Icons.Full iconSize="m" iconColor={theme.colorPrimary} />
</a>

View File

@@ -146,24 +146,23 @@ export function OAuth2RedirectMessage({
const body = (
<p>
This database uses OAuth2 for authentication. Please click the link above
to grant Apache Superset permission to access the data. Your personal
access token will be stored encrypted and used only for queries run by
you.
{t(
'This database uses OAuth2 for authentication. Please click the link above to grant Apache Superset permission to access the data. Your personal access token will be stored encrypted and used only for queries run by you.',
)}
</p>
);
const subtitle = (
<>
You need to{' '}
{t('You need to')}{' '}
<a
href={extra.url}
onClick={handleOAuthClick}
target="_blank"
rel="noreferrer"
>
provide authorization
{t('provide authorization')}
</a>{' '}
in order to run this operation.
{t('in order to run this operation.')}
</>
);

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import { styled, css, SupersetTheme, useTheme } from '@apache-superset/core/ui';
import { t } from '@apache-superset/core';
import cx from 'classnames';
import { Interweave } from 'interweave';
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -152,7 +153,7 @@ export default function Toast({ toast, onCloseToast }: ToastPresenterProps) {
role="button"
tabIndex={0}
onClick={handleClosePress}
aria-label="Close"
aria-label={t('Close')}
data-test="close-button"
/>
</ToastContainer>

View File

@@ -212,7 +212,7 @@ export const DashboardEmbedControls = ({ dashboardId, onHide }: Props) => {
<Input
id="allowed-domains"
value={allowedDomains}
placeholder="superset.example.com"
placeholder={t('superset.example.com')}
onChange={event => setAllowedDomains(event.target.value)}
/>
</FormItem>

View File

@@ -583,7 +583,7 @@ const SliceHeaderControls = (
<Button
id={`slice_${slice.slice_id}-controls`}
buttonStyle="link"
aria-label="More Options"
aria-label={t('More Options')}
aria-haspopup="true"
css={theme => css`
padding: ${theme.sizeUnit * 2}px;

View File

@@ -114,7 +114,7 @@ export default function URLShortLinkButton({
}
/>
&nbsp;&nbsp;
<Typography.Link href={emailLink} aria-label="Email link">
<Typography.Link href={emailLink} aria-label={t('Email link')}>
<Icons.MailOutlined iconSize="m" iconColor={theme.colorPrimary} />
</Typography.Link>
</div>

View File

@@ -142,7 +142,7 @@ test('Popover opens with "Vertical" selected', async () => {
const verticalItem = screen.getByText('Vertical (Left)');
expect(
within(verticalItem.closest('li')!).getByLabelText('check'),
within(verticalItem.closest('li')!).getByLabelText('Selected'),
).toBeInTheDocument();
});
@@ -158,7 +158,7 @@ test('Popover opens with "Horizontal" selected', async () => {
const horizontalItem = screen.getByText('Horizontal (Top)');
expect(
within(horizontalItem.closest('li')!).getByLabelText('check'),
within(horizontalItem.closest('li')!).getByLabelText('Selected'),
).toBeInTheDocument();
});
@@ -182,7 +182,7 @@ test('On selection change, send request and update checked value', async () => {
const verticalItem = await screen.findByText('Vertical (Left)');
expect(
within(verticalItem.closest('li')!).getByLabelText('check'),
within(verticalItem.closest('li')!).getByLabelText('Selected'),
).toBeInTheDocument();
userEvent.click(screen.getByText('Horizontal (Top)'));
@@ -192,7 +192,7 @@ test('On selection change, send request and update checked value', async () => {
const horizontalItem = await screen.findByText('Horizontal (Top)');
expect(
within(horizontalItem.closest('li')!).getByLabelText('check'),
within(horizontalItem.closest('li')!).getByLabelText('Selected'),
).toBeInTheDocument();
await waitFor(() =>
@@ -211,10 +211,10 @@ test('On selection change, send request and update checked value', async () => {
userEvent.hover(screen.getByText('Orientation of filter bar'));
const updatedHorizontalItem = screen.getByText('Horizontal (Top)');
expect(
within(updatedHorizontalItem.closest('li')!).getByLabelText('check'),
within(updatedHorizontalItem.closest('li')!).getByLabelText('Selected'),
).toBeInTheDocument();
expect(
within(verticalItem.closest('li')!).queryByLabelText('check'),
within(verticalItem.closest('li')!).queryByLabelText('Selected'),
).not.toBeInTheDocument();
});
});
@@ -241,10 +241,10 @@ test('On failed request, restore previous selection', async () => {
// Verify initial state
expect(
within(verticalItem.closest('li')!).getByLabelText('check'),
within(verticalItem.closest('li')!).getByLabelText('Selected'),
).toBeInTheDocument();
expect(
within(horizontalItem.closest('li')!).queryByLabelText('check'),
within(horizontalItem.closest('li')!).queryByLabelText('Selected'),
).not.toBeInTheDocument();
// Click horizontal option
@@ -266,10 +266,10 @@ test('On failed request, restore previous selection', async () => {
const verticalItemAfter = screen.getByText('Vertical (Left)');
const horizontalItemAfter = screen.getByText('Horizontal (Top)');
expect(
within(verticalItemAfter.closest('li')!).getByLabelText('check'),
within(verticalItemAfter.closest('li')!).getByLabelText('Selected'),
).toBeInTheDocument();
expect(
within(horizontalItemAfter.closest('li')!).queryByLabelText('check'),
within(horizontalItemAfter.closest('li')!).queryByLabelText('Selected'),
).not.toBeInTheDocument();
});
});

View File

@@ -206,7 +206,7 @@ const FilterBarSettings = () => {
<Icons.CheckOutlined
iconColor={theme.colorPrimary}
iconSize="m"
aria-label="check"
aria-label={t('Selected')}
/>
)}
</Space>
@@ -224,7 +224,7 @@ const FilterBarSettings = () => {
css={css`
vertical-align: middle;
`}
aria-label="check"
aria-label={t('Selected')}
/>
)}
</Space>

View File

@@ -163,7 +163,7 @@ const ConfigModalSidebar: FC<ConfigModalSidebarProps> = ({
onRemove={onRemove}
restoreItem={restoreItem}
dataTestId="filter-title-container"
deleteAltText="RemoveFilter"
deleteAltText={t('Remove filter')}
dragType={FILTER_TYPE}
isCurrentSection={isFilterId(currentItemId)}
onCrossListDrop={handleFilterCrossListDrop}
@@ -185,7 +185,7 @@ const ConfigModalSidebar: FC<ConfigModalSidebarProps> = ({
onRemove={onRemove}
restoreItem={restoreItem}
dataTestId="customization-title-container"
deleteAltText="RemoveCustomization"
deleteAltText={t('Remove customization')}
dragType={CUSTOMIZATION_TYPE}
isCurrentSection={isChartCustomizationId(currentItemId)}
onCrossListDrop={handleCustomizationCrossListDrop}

View File

@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@apache-superset/core';
import { styled } from '@apache-superset/core/ui';
import { useRef, FC } from 'react';
import {
@@ -155,7 +156,7 @@ export const DraggableFilter: FC<FilterTabTitleProps> = ({
<Container ref={ref} isDragging={isDragging}>
<DragIcon
isDragging={isDragging}
alt="Move icon"
alt={t('Move')}
className="dragIcon"
viewBox="4 4 16 16"
/>

View File

@@ -88,7 +88,7 @@ test('drag and drop', async () => {
test('remove filter', async () => {
defaultRender();
// First trash icon
const removeFilterIcon = document.querySelector("[alt='RemoveFilter']")!;
const removeFilterIcon = document.querySelector("[alt='Remove filter']")!;
userEvent.click(removeFilterIcon);
expect(defaultProps.onRemove).toHaveBeenCalledWith('NATIVE_FILTER-1');
});

View File

@@ -161,7 +161,7 @@ const FilterTitleContainer = forwardRef<HTMLDivElement, Props>(
event.stopPropagation();
onRemove(id);
}}
alt="RemoveFilter"
alt={t('Remove filter')}
/>
)}
</div>

View File

@@ -105,7 +105,7 @@ const EmbedCodeContent: FC<EmbedCodeContentProps> = ({
shouldShowText={false}
text={html}
copyNode={
<span role="button" aria-label="Copy to clipboard">
<span role="button" aria-label={t('Copy to clipboard')}>
<Icons.CopyOutlined />
</span>
}

View File

@@ -456,7 +456,7 @@ function PropertiesModal({
bottomSpacing={false}
>
<Input
aria-label="Cache timeout"
aria-label={t('Cache timeout')}
value={cacheTimeout}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setCacheTimeout(event.target.value ?? '')

View File

@@ -616,7 +616,7 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
<Input
name="new_slice_name"
type="text"
placeholder="Name"
placeholder={t('Name')}
value={this.state.newSliceName}
onChange={this.onSliceNameChange}
data-test="new-chart-name"
@@ -631,7 +631,7 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
<Input
name="dataset_name"
type="text"
placeholder="Dataset Name"
placeholder={t('Dataset Name')}
value={this.state.datasetName}
onChange={this.handleDatasetNameChange}
data-test="new-dataset-name"

View File

@@ -1051,7 +1051,7 @@ class AnnotationLayer extends PureComponent<
<>
{this.props.error && (
<span style={{ color: this.props.theme.colorError }}>
ERROR: {this.props.error}
{t('ERROR')}: {this.props.error}
</span>
)}
<div style={{ display: 'flex', flexDirection: 'row' }}>

View File

@@ -206,7 +206,7 @@ class AnnotationLayerControl extends PureComponent<Props, PopoverState> {
);
}
if (!anno.show) {
return <span style={{ color: theme.colorError }}> Hidden </span>;
return <span style={{ color: theme.colorError }}> {t('Hidden')} </span>;
}
return '';
}

View File

@@ -131,7 +131,7 @@ test('Should have remove button', async () => {
test('Should have SortableDragger icon', async () => {
const props = createProps();
render(<CollectionControl {...props} />);
expect(await screen.findByLabelText('drag')).toBeVisible();
expect(await screen.findByLabelText('Drag to reorder')).toBeVisible();
});
test('Should call Control component', async () => {

View File

@@ -72,7 +72,7 @@ const SortableList = SortableContainer(List);
const SortableDragger = SortableHandle(() => (
<Icons.MenuOutlined
role="img"
aria-label="drag"
aria-label={t('Drag to reorder')}
className="text-primary"
style={{ cursor: 'ns-resize' }}
/>

View File

@@ -23,7 +23,7 @@ import {
useState,
MouseEvent as ReactMouseEvent,
} from 'react';
import { t } from '@apache-superset/core';
import { throttle } from 'lodash';
import {
POPOVER_INITIAL_HEIGHT,
@@ -135,7 +135,7 @@ export default function useResizeButton(
return [
<Icons.ArrowsAltOutlined
role="button"
aria-label="Resize"
aria-label={t('Resize')}
tabIndex={0}
onMouseDown={onDragDown}
className="edit-popover-resize"

View File

@@ -470,7 +470,7 @@ export default class AdhocFilterEditPopover extends Component<
</Button>
<Icons.ArrowsAltOutlined
role="button"
aria-label="Resize"
aria-label={t('Resize')}
tabIndex={0}
onMouseDown={this.onDragDown}
className="edit-popover-resize"

View File

@@ -591,7 +591,7 @@ export default class AdhocMetricEditPopover extends PureComponent<
</Button>
<Icons.ArrowsAltOutlined
role="button"
aria-label="Resize"
aria-label={t('Resize')}
tabIndex={0}
onMouseDown={this.onDragDown}
className="edit-popover-resize"

View File

@@ -18,6 +18,7 @@
*/
import { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { t } from '@apache-superset/core';
import { css, SupersetTheme } from '@apache-superset/core/ui';
import { Flex, Icons } from '@superset-ui/core/components';
import { getChartKey } from 'src/explore/exploreUtils';
@@ -55,7 +56,7 @@ export const FastVizSwitcher = memo(
vizTiles.unshift({
name: currentSelection,
icon: CUSTOM_CHART_ICONS[currentSelection] || (
<Icons.MonitorOutlined {...antdIconProps} aria-label="monitor" />
<Icons.MonitorOutlined {...antdIconProps} aria-label={t('Chart')} />
),
});
}

View File

@@ -131,7 +131,7 @@ describe('VizTypeControl', () => {
expect(screen.getByLabelText('pie-chart')).toBeVisible();
expect(screen.getByLabelText('bar-chart')).toBeVisible();
expect(screen.getByLabelText('area-chart')).toBeVisible();
expect(screen.queryByLabelText('monitor')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Chart')).not.toBeInTheDocument();
expect(screen.queryByLabelText('check-square')).not.toBeInTheDocument();
// Multi Chart should NOT appear when other charts are selected
expect(screen.queryByLabelText('multiple')).not.toBeInTheDocument();
@@ -189,7 +189,7 @@ describe('VizTypeControl', () => {
};
await waitForRenderWrapper(props);
expect(screen.getByLabelText('monitor')).toBeVisible();
expect(screen.getByLabelText('Chart')).toBeVisible();
expect(
within(screen.getByTestId('fast-viz-switcher')).getByText('Line Chart'),
).toBeVisible();

View File

@@ -239,7 +239,9 @@ export const ZoomConfigControl: FC<ZoomConfigsControlProps> = ({
min={0}
max={3}
/>
<Tag>Current Zoom: {value?.configs.zoom}</Tag>
<Tag>
{t('Current Zoom')}: {value?.configs.zoom}
</Tag>
</Form>
<ZoomConfigsChart
name="zoomlevels"

View File

@@ -97,7 +97,7 @@ export const httpPath = ({
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.http_path}
placeholder={t('e.g. sql/protocolv1/o/12345')}
label="HTTP Path"
label={t('HTTP Path')}
onChange={changeMethods.onExtraInputChange}
helpText={t('Copy the name of the HTTP Path of your cluster.')}
/>
@@ -187,7 +187,7 @@ export const httpPathField = ({
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.http_path}
placeholder={t('e.g. sql/protocolv1/o/12345')}
label="HTTP Path"
label={t('HTTP Path')}
onChange={changeMethods.onParametersChange}
helpText={t('Copy the name of the HTTP Path of your cluster.')}
/>
@@ -335,7 +335,7 @@ export const forceSSLField = ({
});
}}
/>
<span css={toggleStyle}>SSL</span>
<span css={toggleStyle}>{t('SSL')}</span>
<InfoTooltip
tooltip={t('SSL Mode "require" will be used.')}
placement="right"
@@ -359,7 +359,7 @@ export const projectIdfield = ({
value={db?.parameters?.project_id}
validationMethods={{ onBlur: getValidation }}
errorMessage={validationErrors?.project_id}
placeholder="your-project-1234-a1"
placeholder={t('your-project-1234-a1')}
label={t('Project Id')}
onChange={changeMethods.onParametersChange}
helpText={t('Enter the unique project id for your database.')}

View File

@@ -18,15 +18,16 @@
*/
import { useState } from 'react';
import { t } from '@apache-superset/core';
import { Input, Collapse, FormItem } from '@superset-ui/core/components';
import { CustomParametersChangeType, FieldPropTypes } from '../../types';
const LABELS = {
CLIENT_ID: 'Client ID',
SECRET: 'Client Secret',
AUTH_URI: 'Authorization Request URI',
TOKEN_URI: 'Token Request URI',
SCOPE: 'Scope',
CLIENT_ID: t('Client ID'),
SECRET: t('Client Secret'),
AUTH_URI: t('Authorization Request URI'),
TOKEN_URI: t('Token Request URI'),
SCOPE: t('Scope'),
};
interface OAuth2ClientInfo {
@@ -81,7 +82,7 @@ export const OAuth2ClientField = ({
items={[
{
key: 'oauth2-client-information',
label: 'OAuth2 client information',
label: t('OAuth2 client information'),
children: (
<>
<FormItem label={LABELS.CLIENT_ID}>

View File

@@ -22,19 +22,19 @@ import { DatabaseParameters, FieldPropTypes } from '../../types';
const FIELD_TEXT_MAP = {
account: {
label: 'Account',
label: t('Account'),
helpText: t(
'Copy the identifier of the account you are trying to connect to.',
),
placeholder: t('e.g. xy12345.us-east-2.aws'),
},
warehouse: {
label: 'Warehouse',
label: t('Warehouse'),
placeholder: t('e.g. compute_wh'),
className: 'form-group-w-50',
},
role: {
label: 'Role',
label: t('Role'),
placeholder: t('e.g. AccountAdmin'),
className: 'form-group-w-50',
},

View File

@@ -537,7 +537,7 @@ const ExtraOptions = ({
type="text"
name="schemas_allowed_for_file_upload"
value={schemasText}
placeholder="schema1,schema2"
placeholder={t('schema1,schema2')}
onChange={e => setSchemasText(e.target.value)}
onBlur={() =>
onExtraInputChange({

View File

@@ -159,11 +159,11 @@ const SSHTunnelForm = ({
data-test="ssh-tunnel-password-input"
iconRender={visible =>
visible ? (
<Tooltip title="Hide password.">
<Tooltip title={t('Hide password.')}>
<Icons.EyeInvisibleOutlined />
</Tooltip>
) : (
<Tooltip title="Show password.">
<Tooltip title={t('Show password.')}>
<Icons.EyeOutlined />
</Tooltip>
)
@@ -207,11 +207,11 @@ const SSHTunnelForm = ({
data-test="ssh-tunnel-private_key_password-input"
iconRender={visible =>
visible ? (
<Tooltip title="Hide password.">
<Tooltip title={t('Hide password.')}>
<Icons.EyeInvisibleOutlined />
</Tooltip>
) : (
<Tooltip title="Show password.">
<Tooltip title={t('Show password.')}>
<Icons.EyeOutlined />
</Tooltip>
)

View File

@@ -114,13 +114,14 @@ const TABS_KEYS = {
const engineSpecificAlertMapping = {
[Engines.GSheet]: {
message: 'Why do I need to create a database?',
description:
message: t('Why do I need to create a database?'),
description: t(
'To begin using your Google Sheets, you need to create a database first. ' +
'Databases are used as a way to identify ' +
'your data so that it can be queried and visualized. This ' +
'database will hold all of your individual Google Sheets ' +
'you choose to connect here.',
'Databases are used as a way to identify ' +
'your data so that it can be queried and visualized. This ' +
'database will hold all of your individual Google Sheets ' +
'you choose to connect here.',
),
},
};

View File

@@ -40,7 +40,7 @@ const ColumnsPreview: FC<ColumnsPreviewProps> = ({
return (
<StyledDivContainer>
<Typography.Text type="secondary">Columns:</Typography.Text>
<Typography.Text type="secondary">{t('Columns')}:</Typography.Text>
{columns.length === 0 ? (
<p className="help-block">{t('Upload file to preview columns')}</p>
) : (

View File

@@ -731,7 +731,10 @@ const UploadDataModal: FunctionComponent<UploadDataModalProps> = ({
name="table_name"
required
rules={[
{ required: true, message: 'Table name is required' },
{
required: true,
message: t('Table name is required'),
},
]}
>
<Input
@@ -1024,7 +1027,7 @@ const UploadDataModal: FunctionComponent<UploadDataModalProps> = ({
rules={[
{
required: true,
message: 'Header row is required',
message: t('Header row is required'),
},
]}
>
@@ -1057,7 +1060,7 @@ const UploadDataModal: FunctionComponent<UploadDataModalProps> = ({
rules={[
{
required: true,
message: 'Skip rows is required',
message: t('Skip rows is required'),
},
]}
>

View File

@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@apache-superset/core';
export default function RightPanel() {
return <div>Right Panel</div>;
return <div>{t('Right Panel')}</div>;
}

View File

@@ -95,7 +95,7 @@ function UserInfoModal({
>
<Input.Password
name="password"
placeholder="Enter the user's password"
placeholder={t("Enter the user's password")}
/>
</FormItem>
<FormItem

View File

@@ -236,7 +236,7 @@ function UserListModal({
>
<Input.Password
name="password"
placeholder="Enter the user's password"
placeholder={t("Enter the user's password")}
/>
</FormItem>
<FormItem

View File

@@ -86,8 +86,10 @@ export default function Login() {
>
<Result
status="success"
title="Registration successful"
subTitle="Your account is activated. You can log in with your credentials."
title={t('Registration successful')}
subTitle={t(
'Your account is activated. You can log in with your credentials.',
)}
extra={[
<Button type="default" href="/login/" data-test="login-button">
{t('Login')}
@@ -220,7 +222,7 @@ export default function Login() {
/>
</Form.Item>
{authRecaptchaPublicKey && (
<Form.Item label="Captcha">
<Form.Item label={t('Captcha')}>
<ReactCAPTCHA
sitekey={authRecaptchaPublicKey}
onChange={value => {

View File

@@ -105,7 +105,7 @@ export function UserInfo({ user }: { user: UserWithPermissionsAndRoles }) {
setUserDetails(transformedUser);
})
.catch(error => {
addDangerToast('Failed to fetch user info:', error);
addDangerToast(`${t('Failed to fetch user info')}:`, error);
});
}, [userDetails]);
@@ -157,11 +157,11 @@ export function UserInfo({ user }: { user: UserWithPermissionsAndRoles }) {
return (
<StyledLayout>
<StyledHeader>Your user information</StyledHeader>
<StyledHeader>{t('Your user information')}</StyledHeader>
<DescriptionsContainer>
<Collapse defaultActiveKey={['userInfo', 'personalInfo']} ghost>
<Collapse.Panel
header={<DescriptionTitle>User info</DescriptionTitle>}
header={<DescriptionTitle>{t('User info')}</DescriptionTitle>}
key="userInfo"
>
<Descriptions
@@ -170,22 +170,22 @@ export function UserInfo({ user }: { user: UserWithPermissionsAndRoles }) {
column={1}
labelStyle={{ width: '120px' }}
>
<Descriptions.Item label="User Name">
<Descriptions.Item label={t('User Name')}>
{user.username}
</Descriptions.Item>
<Descriptions.Item label="Is Active?">
{user.isActive ? 'Yes' : 'No'}
<Descriptions.Item label={t('Is Active?')}>
{user.isActive ? t('Yes') : t('No')}
</Descriptions.Item>
<Descriptions.Item label="Role">
{user.roles ? Object.keys(user.roles).join(', ') : 'None'}
<Descriptions.Item label={t('Role')}>
{user.roles ? Object.keys(user.roles).join(', ') : t('None')}
</Descriptions.Item>
<Descriptions.Item label="Login count">
<Descriptions.Item label={t('Login count')}>
{user.loginCount}
</Descriptions.Item>
</Descriptions>
</Collapse.Panel>
<Collapse.Panel
header={<DescriptionTitle>Personal info</DescriptionTitle>}
header={<DescriptionTitle>{t('Personal info')}</DescriptionTitle>}
key="personalInfo"
>
<Descriptions
@@ -194,13 +194,15 @@ export function UserInfo({ user }: { user: UserWithPermissionsAndRoles }) {
column={1}
labelStyle={{ width: '120px' }}
>
<Descriptions.Item label="First Name">
<Descriptions.Item label={t('First Name')}>
{userDetails.firstName}
</Descriptions.Item>
<Descriptions.Item label="Last Name">
<Descriptions.Item label={t('Last Name')}>
{userDetails.lastName}
</Descriptions.Item>
<Descriptions.Item label="Email">{user.email}</Descriptions.Item>
<Descriptions.Item label={t('Email')}>
{user.email}
</Descriptions.Item>
</Descriptions>
</Collapse.Panel>
</Collapse>