mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(console,ui,demo-app): fix i18next types (#3743)
* refactor(console,ui,demo-app): fix i18next types disable allowObjectInHTMLChildren for safer typing. use <DynamicT /> for all dynamic translations. * fix(console): i18n key in UserAccountInformation * refactor(console,ui): update <DynamicT /> and add tests
This commit is contained in:
parent
ac95eb3ffa
commit
20418dc4f9
36 changed files with 292 additions and 204 deletions
33
packages/console/jest.config.ts
Normal file
33
packages/console/jest.config.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import type { Config } from '@jest/types';
|
||||
|
||||
const config: Config.InitialOptions = {
|
||||
roots: ['<rootDir>/src'],
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': [
|
||||
'@swc/jest',
|
||||
{
|
||||
sourceMaps: true,
|
||||
jsc: {
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'automatic',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'\\.(svg)$': 'jest-transformer-svg',
|
||||
'\\.(png)$': 'jest-transform-stub',
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^@logto/app-insights/(.*)$': '<rootDir>/../app-insights/lib/$1',
|
||||
'^@logto/shared/(.*)$': '<rootDir>/../shared/lib/$1',
|
||||
'\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto|@silverhand))/)'],
|
||||
};
|
||||
|
||||
export default config;
|
9
packages/console/jest.setup.ts
Normal file
9
packages/console/jest.setup.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
void i18next.use(initReactI18next).init({
|
||||
// Simple resources for testing
|
||||
resources: { en: { translation: { admin_console: { general: { add: 'Add' } } } } },
|
||||
lng: 'en',
|
||||
react: { useSuspense: false },
|
||||
});
|
|
@ -18,10 +18,13 @@
|
|||
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --no-cache --public-url ${CONSOLE_PUBLIC_URL:-/console}",
|
||||
"lint": "eslint --ext .ts --ext .tsx src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"stylelint": "stylelint \"src/**/*.scss\""
|
||||
"stylelint": "stylelint \"src/**/*.scss\"",
|
||||
"test:ci": "jest --coverage --silent",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/roboto-mono": "^4.5.7",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@logto/app-insights": "workspace:^1.2.0",
|
||||
"@logto/connector-kit": "workspace:^1.1.1",
|
||||
"@logto/core-kit": "workspace:^2.0.0",
|
||||
|
@ -43,8 +46,12 @@
|
|||
"@silverhand/essentials": "^2.5.0",
|
||||
"@silverhand/ts-config": "3.0.0",
|
||||
"@silverhand/ts-config-react": "3.0.0",
|
||||
"@swc/core": "^1.3.52",
|
||||
"@swc/jest": "^0.2.26",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/mdx": "^2.0.1",
|
||||
"@types/mdx-js__react": "^1.5.5",
|
||||
"@types/react": "^18.0.31",
|
||||
|
@ -67,6 +74,11 @@
|
|||
"history": "^5.3.0",
|
||||
"i18next": "^22.4.15",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-transform-stub": "^2.0.0",
|
||||
"jest-transformer-svg": "^2.0.0",
|
||||
"just-kebab-case": "^4.2.0",
|
||||
"ky": "^0.33.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Info from '@/assets/images/info.svg';
|
||||
|
||||
import Button from '../Button';
|
||||
import DynamicT from '../DynamicT';
|
||||
import TextLink from '../TextLink';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -29,15 +29,17 @@ function Alert({
|
|||
variant = 'plain',
|
||||
className,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.alert, styles[severity], styles[variant], className)}>
|
||||
<div className={styles.icon}>
|
||||
<Info />
|
||||
</div>
|
||||
<div className={styles.content}>{children}</div>
|
||||
{action && href && <TextLink to={href}>{t(action)}</TextLink>}
|
||||
{action && href && (
|
||||
<TextLink to={href}>
|
||||
<DynamicT forKey={action} />
|
||||
</TextLink>
|
||||
)}
|
||||
{action && onClick && (
|
||||
<div className={styles.action}>
|
||||
<Button title={action} type="text" size="small" onClick={onClick} />
|
||||
|
|
|
@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { Ring as Spinner } from '@/components/Spinner';
|
||||
|
||||
import type DangerousRaw from '../DangerousRaw';
|
||||
import DynamicT from '../DynamicT';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -96,7 +97,14 @@ function Button({
|
|||
>
|
||||
{showSpinner && <Spinner className={styles.spinner} />}
|
||||
{icon && <span className={styles.icon}>{icon}</span>}
|
||||
{title && (typeof title === 'string' ? <span>{t(title)}</span> : title)}
|
||||
{title &&
|
||||
(typeof title === 'string' ? (
|
||||
<span>
|
||||
<DynamicT forKey={title} />
|
||||
</span>
|
||||
) : (
|
||||
title
|
||||
))}
|
||||
{trailingIcon && <span className={styles.trailingIcon}>{trailingIcon}</span>}
|
||||
</button>
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { ReactElement } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type DangerousRaw from '../DangerousRaw';
|
||||
import DynamicT from '../DynamicT';
|
||||
import TextLink from '../TextLink';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -33,11 +34,13 @@ function CardTitle({
|
|||
return (
|
||||
<div className={classNames(styles.container, styles[size], className)}>
|
||||
<div className={classNames(styles.title, !isWordWrapEnabled && styles.titleEllipsis)}>
|
||||
{typeof title === 'string' ? t(title) : title}
|
||||
{typeof title === 'string' ? <DynamicT forKey={title} /> : title}
|
||||
</div>
|
||||
{Boolean(subtitle ?? learnMoreLink) && (
|
||||
<div className={styles.subtitle}>
|
||||
{subtitle && <span>{typeof subtitle === 'string' ? t(subtitle) : subtitle}</span>}
|
||||
{subtitle && (
|
||||
<span>{typeof subtitle === 'string' ? <DynamicT forKey={subtitle} /> : subtitle}</span>
|
||||
)}
|
||||
{learnMoreLink && (
|
||||
<TextLink href={learnMoreLink} target="_blank" className={styles.learnMore}>
|
||||
{t('general.learn_more')}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Back from '@/assets/images/back.svg';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
|
||||
import type DangerousRaw from '../DangerousRaw';
|
||||
import DetailsSkeleton from '../DetailsSkeleton';
|
||||
import DynamicT from '../DynamicT';
|
||||
import RequestDataError from '../RequestDataError';
|
||||
import TextLink from '../TextLink';
|
||||
|
||||
|
@ -32,12 +32,10 @@ function DetailsPage({
|
|||
children,
|
||||
className,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<TextLink to={backLink} icon={<Back />} className={styles.backLink}>
|
||||
{typeof backLinkTitle === 'string' ? t(backLinkTitle) : backLinkTitle}
|
||||
{typeof backLinkTitle === 'string' ? <DynamicT forKey={backLinkTitle} /> : backLinkTitle}
|
||||
</TextLink>
|
||||
{isLoading ? (
|
||||
<DetailsSkeleton />
|
||||
|
|
22
packages/console/src/components/DynamicT/index.test.tsx
Normal file
22
packages/console/src/components/DynamicT/index.test.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { t, type TFuncKey, type TypeOptions } from 'i18next';
|
||||
|
||||
import DynamicT from '.';
|
||||
|
||||
describe('<DynamicT />', () => {
|
||||
it('should render a correct key', () => {
|
||||
const key: TFuncKey<TypeOptions['defaultNS'], 'admin_console'> = 'general.add';
|
||||
const { container } = render(<DynamicT forKey={key} />);
|
||||
|
||||
expect(container.innerHTML).toBe(t(`admin_console.${key}`));
|
||||
});
|
||||
|
||||
it('should render an error message for a non-leaf key', () => {
|
||||
const key: TFuncKey<TypeOptions['defaultNS'], 'admin_console'> = 'general';
|
||||
const { container } = render(<DynamicT forKey={key} />);
|
||||
|
||||
expect(container.innerHTML).toBe(
|
||||
`key 'admin_console.${key} (en)' returned an object instead of string.`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -3,16 +3,29 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
type Props = {
|
||||
forKey: AdminConsoleKey;
|
||||
interpolation?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A component to render a dynamic translation key.
|
||||
* Since `ReactNode` does not include vanilla objects while `JSX.Element` does. It's strange but no better way for now.
|
||||
*
|
||||
* @see https://github.com/i18next/i18next/issues/1852
|
||||
*/
|
||||
export default function DynamicT({ forKey }: Props) {
|
||||
export default function DynamicT({ forKey, interpolation }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return <>{t(forKey)}</>;
|
||||
/**
|
||||
* The default return of `t()` is already string even if the given key is not a leaf key.
|
||||
* For example:
|
||||
*
|
||||
* ```ts
|
||||
* const translation = { foo: { bar: 'baz' } };
|
||||
*
|
||||
* t('foo.bar') // 'baz'
|
||||
* t('foo') // 'key 'foo (en)' returned an object instead of string.'
|
||||
* ```
|
||||
*
|
||||
* So actually there's no need to check key validity to make sure `t()` returns a string.
|
||||
* But it seems the type definition is not correct for the function in `i18next`. Use this trick to
|
||||
* bypass for now.
|
||||
*/
|
||||
return <>{t(forKey, interpolation ?? {})}</>;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ function FormCard({ title, description, learnMoreLink, children }: Props) {
|
|||
return (
|
||||
<Card className={styles.container}>
|
||||
<div className={styles.introduction}>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
<div className={styles.title}>
|
||||
<DynamicT forKey={title} />
|
||||
</div>
|
||||
{description && (
|
||||
<div className={styles.description}>
|
||||
<DynamicT forKey={description} />
|
||||
|
|
|
@ -17,6 +17,7 @@ import { Tooltip } from '@/components/Tip';
|
|||
import { ApiResourceDetailsTabs } from '@/consts/page-tabs';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
|
||||
import DynamicT from '../DynamicT';
|
||||
import EmptyDataPlaceholder from '../EmptyDataPlaceholder';
|
||||
import type { Props as PaginationProps } from '../Pagination';
|
||||
import TablePlaceholder from '../Table/TablePlaceholder';
|
||||
|
@ -101,7 +102,7 @@ function PermissionsTable({
|
|||
* When the table is read-only, hide the delete button rather than the whole column to keep the table column spaces.
|
||||
*/
|
||||
isReadOnly ? null : (
|
||||
<Tooltip content={<div>{t(deleteButtonTitle)}</div>}>
|
||||
<Tooltip content={<DynamicT forKey={deleteButtonTitle} />}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
deleteHandler(scope);
|
||||
|
|
|
@ -2,7 +2,6 @@ import type { AdminConsoleKey } from '@logto/phrases';
|
|||
import classNames from 'classnames';
|
||||
import type { KeyboardEventHandler, ReactElement, ReactNode } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type DangerousRaw from '../DangerousRaw';
|
||||
import DynamicT from '../DynamicT';
|
||||
|
@ -49,8 +48,6 @@ function Radio({
|
|||
disabledLabel,
|
||||
icon,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const handleKeyPress: KeyboardEventHandler<HTMLDivElement> = useCallback(
|
||||
(event) => {
|
||||
if (isDisabled) {
|
||||
|
@ -93,7 +90,7 @@ function Radio({
|
|||
{title && (typeof title === 'string' ? <DynamicT forKey={title} /> : title)}
|
||||
{isDisabled && disabledLabel && (
|
||||
<div className={classNames(styles.indicator, styles.disabledLabel)}>
|
||||
{t(disabledLabel)}
|
||||
<DynamicT forKey={disabledLabel} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -26,7 +26,9 @@ function TablePlaceholder({ image, imageDark, title, description, learnMoreLink,
|
|||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<div className={styles.image}>{theme === Theme.Light ? image : imageDark}</div>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
<div className={styles.title}>
|
||||
<DynamicT forKey={title} />
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<DynamicT forKey={description} />
|
||||
{learnMoreLink && (
|
||||
|
|
|
@ -46,7 +46,7 @@ function UserAccountInformation({
|
|||
primaryEmail && `${t('user_details.created_email')} ${primaryEmail}`,
|
||||
primaryPhone && `${t('user_details.created_phone')} ${primaryPhone}`,
|
||||
username && `${t('user_details.created_username')} ${username}`,
|
||||
`${passwordLabel ?? t('user_details.created_username')} ${password}`
|
||||
`${passwordLabel ?? t('user_details.created_password')} ${password}`
|
||||
).join('\n');
|
||||
|
||||
await navigator.clipboard.writeText(content);
|
||||
|
|
|
@ -2,11 +2,11 @@ import type { AdminConsoleKey } from '@logto/phrases';
|
|||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ArrowRight from '@/assets/images/arrow-right.svg';
|
||||
import Tick from '@/assets/images/tick.svg';
|
||||
import { DropdownItem } from '@/components/Dropdown';
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
import type { Option } from '@/components/Select';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
@ -32,7 +32,6 @@ function SubMenu<T extends string>({
|
|||
selectedOption,
|
||||
onItemClick,
|
||||
}: Props<T>) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const mouseEnterTimeoutRef = useRef(0);
|
||||
|
@ -67,7 +66,9 @@ function SubMenu<T extends string>({
|
|||
}}
|
||||
>
|
||||
{icon && <span className={styles.icon}>{icon}</span>}
|
||||
<span className={styles.title}>{t(title)}</span>
|
||||
<span className={styles.title}>
|
||||
<DynamicT forKey={title} />
|
||||
</span>
|
||||
<Spacer />
|
||||
<ArrowRight className={styles.icon} />
|
||||
<div className={classNames(styles.menu, showMenu && styles.visible)}>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
|
@ -14,7 +14,6 @@ type Props = {
|
|||
};
|
||||
|
||||
function Contact({ isOpen, onCancel }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const contacts = useContacts();
|
||||
|
||||
return (
|
||||
|
@ -33,8 +32,12 @@ function Contact({ isOpen, onCancel }: Props) {
|
|||
<ContactIcon />
|
||||
</div>
|
||||
<div className={styles.text}>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
<div className={styles.description}>{t(description)}</div>
|
||||
<div className={styles.title}>
|
||||
<DynamicT forKey={title} />
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<DynamicT forKey={description} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
|
|
2
packages/console/src/include.d/i18next.d.ts
vendored
2
packages/console/src/include.d/i18next.d.ts
vendored
|
@ -4,8 +4,6 @@ import type { LocalePhrase } from '@logto/phrases';
|
|||
|
||||
declare module 'i18next' {
|
||||
interface CustomTypeOptions {
|
||||
returnNull: false;
|
||||
allowObjectInHTMLChildren: true;
|
||||
resources: LocalePhrase;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
import { Tooltip } from '@/components/Tip';
|
||||
|
@ -23,10 +22,8 @@ function CardItem({
|
|||
onClick,
|
||||
className,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
<Tooltip content={conditional(isDisabled && disabledTip && <>{t(disabledTip)}</>)}>
|
||||
<Tooltip content={conditional(isDisabled && disabledTip && <DynamicT forKey={disabledTip} />)}>
|
||||
<div
|
||||
key={value}
|
||||
role="button"
|
||||
|
@ -55,10 +52,16 @@ function CardItem({
|
|||
<div>
|
||||
{typeof title === 'string' ? <DynamicT forKey={title} /> : title}
|
||||
{trailingTag && (
|
||||
<span className={classNames(styles.tag, styles.trailingTag)}>{t(trailingTag)}</span>
|
||||
<span className={classNames(styles.tag, styles.trailingTag)}>
|
||||
<DynamicT forKey={trailingTag} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{tag && <span className={styles.tag}>{t(tag)}</span>}
|
||||
{tag && (
|
||||
<span className={styles.tag}>
|
||||
<DynamicT forKey={tag} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
|
|
@ -2,9 +2,9 @@ import type { AdminConsoleKey } from '@logto/phrases';
|
|||
import classNames from 'classnames';
|
||||
import { cloneElement } from 'react';
|
||||
import type { ReactNode, ReactElement } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -19,15 +19,17 @@ type Props = {
|
|||
};
|
||||
|
||||
function ReachLogto({ title, description, icon, buttonTitle, buttonIcon, link, className }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.reachLogto, className)}>
|
||||
<div className={styles.reachLogtoInfo}>
|
||||
{cloneElement(icon, { className: styles.reachLogtoIcon })}
|
||||
<div>
|
||||
<div className={styles.reachLogtoTitle}>{t(title)}</div>
|
||||
<div className={styles.reachLogtoDescription}>{t(description)}</div>
|
||||
<div className={styles.reachLogtoTitle}>
|
||||
<DynamicT forKey={title} />
|
||||
</div>
|
||||
<div className={styles.reachLogtoDescription}>
|
||||
<DynamicT forKey={description} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
|
|
|
@ -26,7 +26,7 @@ function ConnectorTypeColumn({ connectorGroup: { type, connectors } }: Props) {
|
|||
);
|
||||
|
||||
if (!firstStandardConnector) {
|
||||
return <>{t(connectorTitlePlaceHolder[type])}</>;
|
||||
return <span>{t(connectorTitlePlaceHolder[type])}</span>;
|
||||
}
|
||||
|
||||
if (!connectorFactory) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import CompleteIndicator from '@/assets/images/task-complete.svg';
|
|||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import ConfirmModal from '@/components/ConfirmModal';
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
|
@ -73,8 +74,12 @@ function GetStarted() {
|
|||
{!isComplete && <CardIcon className={styles.icon} />}
|
||||
{isComplete && <CompleteIndicator className={styles.icon} />}
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
<div className={styles.subtitle}>{t(subtitle)}</div>
|
||||
<div className={styles.title}>
|
||||
<DynamicT forKey={title} />
|
||||
</div>
|
||||
<div className={styles.subtitle}>
|
||||
<DynamicT forKey={subtitle} />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.button}
|
||||
|
|
|
@ -2,7 +2,6 @@ import type { AdminConsoleKey } from '@logto/phrases';
|
|||
import type { Nullable } from '@silverhand/essentials';
|
||||
import type { ReactElement } from 'react';
|
||||
import { cloneElement } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
|
@ -34,7 +33,6 @@ function CardContent<T extends Nullable<boolean | string | Record<string, unknow
|
|||
title,
|
||||
data,
|
||||
}: Props<T>) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const defaultRenderer = (value: unknown) => (value ? <span>{String(value)}</span> : <NotSet />);
|
||||
|
||||
if (data.length === 0) {
|
||||
|
@ -43,7 +41,9 @@ function CardContent<T extends Nullable<boolean | string | Record<string, unknow
|
|||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
<div className={styles.title}>
|
||||
<DynamicT forKey={title} />
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
{data.map(({ key, icon, label, value, renderer = defaultRenderer, action }) => {
|
||||
|
|
|
@ -6,6 +6,7 @@ import ReactModal from 'react-modal';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Arrow from '@/assets/images/arrow-left.svg';
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
import TextLink from '@/components/TextLink';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
|
@ -40,7 +41,9 @@ function MainFlowLikeModal({ title, subtitle, subtitleProps, children, onClose,
|
|||
>
|
||||
{t('general.back')}
|
||||
</TextLink>
|
||||
<span className={styles.title}>{t(title)}</span>
|
||||
<span className={styles.title}>
|
||||
<DynamicT forKey={title} />
|
||||
</span>
|
||||
{subtitle && (
|
||||
<span className={styles.subtitle}>
|
||||
<Trans components={{ strong: <span className={styles.strong} /> }}>
|
||||
|
|
|
@ -10,6 +10,6 @@
|
|||
},
|
||||
"include": [
|
||||
"src",
|
||||
"jest.config.ts"
|
||||
"jest.*.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { CustomTypeOptions } from 'react-i18next';
|
|||
|
||||
declare module 'react-i18next' {
|
||||
interface CustomTypeOptions {
|
||||
allowObjectInHTMLChildren: true;
|
||||
resources: LocalePhrase;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { TFuncKey } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
import NavBar from '@/components/NavBar';
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
import usePlatform from '@/hooks/use-platform';
|
||||
|
@ -26,7 +26,6 @@ const SecondaryPageLayout = ({
|
|||
notification,
|
||||
children,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = usePlatform();
|
||||
|
||||
return (
|
||||
|
@ -38,9 +37,13 @@ const SecondaryPageLayout = ({
|
|||
)}
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{t(title, titleProps ?? {})}</div>
|
||||
<div className={styles.title}>
|
||||
<DynamicT forKey={title} interpolation={titleProps} />
|
||||
</div>
|
||||
{description && (
|
||||
<div className={styles.description}>{t(description, descriptionProps ?? {})}</div>
|
||||
<div className={styles.description}>
|
||||
<DynamicT forKey={description} interpolation={descriptionProps} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { Nullable } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import type { TFuncKey } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DynamicT from '../DynamicT';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -12,12 +13,14 @@ export type Props = {
|
|||
};
|
||||
|
||||
const BrandingHeader = ({ logo, headline, className }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
{logo && <img className={styles.logo} alt="app logo" src={logo} crossOrigin="anonymous" />}
|
||||
{headline && <div className={styles.headline}>{t(headline)}</div>}
|
||||
{headline && (
|
||||
<div className={styles.headline}>
|
||||
<DynamicT forKey={headline} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import classNames from 'classnames';
|
||||
import type { TFuncKey } from 'i18next';
|
||||
import type { HTMLProps } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DynamicT from '../DynamicT';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -32,8 +33,6 @@ const Button = ({
|
|||
onClick,
|
||||
...rest
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<button
|
||||
disabled={isDisabled}
|
||||
|
@ -48,7 +47,7 @@ const Button = ({
|
|||
onClick={onClick}
|
||||
{...rest}
|
||||
>
|
||||
{t(title, { ...i18nProps })}
|
||||
<DynamicT forKey={title} interpolation={i18nProps} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
26
packages/ui/src/components/DynamicT/index.test.tsx
Normal file
26
packages/ui/src/components/DynamicT/index.test.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { t, type TFuncKey } from 'i18next';
|
||||
|
||||
import DynamicT from '.';
|
||||
|
||||
describe('<DynamicT />', () => {
|
||||
it('should render empty string when no key passed', () => {
|
||||
const { container } = render(<DynamicT />);
|
||||
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('should render a correct key', () => {
|
||||
const key: TFuncKey = 'action.agree';
|
||||
const { container } = render(<DynamicT forKey={key} />);
|
||||
|
||||
expect(container.innerHTML).toBe(t(key));
|
||||
});
|
||||
|
||||
it('should render an error message for a non-leaf key', () => {
|
||||
const key: TFuncKey = 'action';
|
||||
const { container } = render(<DynamicT forKey={key} />);
|
||||
|
||||
expect(container.innerHTML).toBe(`key '${key} (en)' returned an object instead of string.`);
|
||||
});
|
||||
});
|
|
@ -3,21 +3,29 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
type Props = {
|
||||
forKey?: TFuncKey;
|
||||
interpolation?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A component to render a dynamic translation key.
|
||||
* Since `ReactNode` does not include vanilla objects while `JSX.Element` does. It's strange but no better way for now.
|
||||
*
|
||||
* @see https://github.com/i18next/i18next/issues/1852
|
||||
*/
|
||||
const DynamicT = ({ forKey }: Props) => {
|
||||
const DynamicT = ({ forKey, interpolation }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!forKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{t(forKey)}</>;
|
||||
const translated = t(forKey, interpolation ?? {});
|
||||
|
||||
if (typeof translated === 'string') {
|
||||
// The fragment will ensure the component has the return type that is compatible with `JSX.Element`.
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{translated}</>;
|
||||
}
|
||||
|
||||
// The fragment will ensure the component has the return type that is compatible with `JSX.Element`.
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{`Translation key ${forKey} is invalid.`}</>; // This would be great to have i18n as well. Not harmful for now.
|
||||
};
|
||||
export default DynamicT;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import classNames from 'classnames';
|
||||
import type { TFuncKey } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -10,9 +11,11 @@ type Props = {
|
|||
};
|
||||
|
||||
const InlineNotification = ({ className, message }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className={classNames(styles.notification, className)}>{t(message)}</div>;
|
||||
return (
|
||||
<div className={classNames(styles.notification, className)}>
|
||||
<DynamicT forKey={message} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InlineNotification;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import Button from '@/components/Button';
|
||||
import Divider from '@/components/Divider';
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import useBindSocialRelatedUser from '@/hooks/use-social-link-related-user';
|
||||
import useSocialRegister from '@/hooks/use-social-register';
|
||||
|
@ -80,7 +81,9 @@ const SocialLinkAccount = ({ connectorId, className, relatedUser }: Props) => {
|
|||
|
||||
<Divider label="description.or" className={styles.divider} />
|
||||
|
||||
<div className={styles.desc}>{t(content.desc)}</div>
|
||||
<div className={styles.desc}>
|
||||
<DynamicT forKey={content.desc} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
title={content.buttonText}
|
||||
|
|
1
packages/ui/src/include.d/i18next.d.ts
vendored
1
packages/ui/src/include.d/i18next.d.ts
vendored
|
@ -4,7 +4,6 @@ import type { LocalePhrase } from '@logto/phrases-ui';
|
|||
|
||||
declare module 'i18next' {
|
||||
interface CustomTypeOptions {
|
||||
allowObjectInHTMLChildren: true;
|
||||
resources: LocalePhrase;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
|
||||
import i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
|
@ -15,15 +19,9 @@ Object.defineProperty(window, 'matchMedia', {
|
|||
})),
|
||||
});
|
||||
|
||||
const translation = (key: string) => key;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
jest.mock('react-i18next', () => ({
|
||||
...jest.requireActual('react-i18next'),
|
||||
useTranslation: () => ({
|
||||
t: translation,
|
||||
i18n: {
|
||||
t: translation,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
void i18next.use(initReactI18next).init({
|
||||
// Simple resources for testing
|
||||
resources: { en: { translation: { action: { agree: 'Agree' } } } },
|
||||
lng: 'en',
|
||||
react: { useSuspense: false },
|
||||
});
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import type { TFuncKey } from 'i18next';
|
||||
import { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import StaticPageLayout from '@/Layout/StaticPageLayout';
|
||||
import PageContext from '@/Providers/PageContextProvider/PageContext';
|
||||
import EmptyStateDark from '@/assets/icons/empty-state-dark.svg';
|
||||
import EmptyState from '@/assets/icons/empty-state.svg';
|
||||
import DynamicT from '@/components/DynamicT';
|
||||
import NavBar from '@/components/NavBar';
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
|
||||
|
@ -19,11 +19,8 @@ type Props = {
|
|||
};
|
||||
|
||||
const ErrorPage = ({ title = 'description.not_found', message, rawMessage }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { theme } = useContext(PageContext);
|
||||
|
||||
const errorMessage = rawMessage ?? (message && t(message));
|
||||
const errorMessage = Boolean(rawMessage ?? message);
|
||||
|
||||
return (
|
||||
<StaticPageLayout>
|
||||
|
@ -31,8 +28,12 @@ const ErrorPage = ({ title = 'description.not_found', message, rawMessage }: Pro
|
|||
{history.length > 1 && <NavBar />}
|
||||
<div className={styles.container}>
|
||||
{theme === Theme.Light ? <EmptyState /> : <EmptyStateDark />}
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
{errorMessage && <div className={styles.message}>{String(errorMessage)}</div>}
|
||||
<div className={styles.title}>
|
||||
<DynamicT forKey={title} />
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<div className={styles.message}>{rawMessage ?? <DynamicT forKey={message} />}</div>
|
||||
)}
|
||||
</div>
|
||||
</StaticPageLayout>
|
||||
);
|
||||
|
|
145
pnpm-lock.yaml
145
pnpm-lock.yaml
|
@ -2765,6 +2765,9 @@ importers:
|
|||
'@fontsource/roboto-mono':
|
||||
specifier: ^4.5.7
|
||||
version: 4.5.7
|
||||
'@jest/types':
|
||||
specifier: ^29.5.0
|
||||
version: 29.5.0
|
||||
'@logto/app-insights':
|
||||
specifier: workspace:^1.2.0
|
||||
version: link:../app-insights
|
||||
|
@ -2828,12 +2831,24 @@ importers:
|
|||
'@silverhand/ts-config-react':
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0(typescript@5.0.2)
|
||||
'@swc/core':
|
||||
specifier: ^1.3.52
|
||||
version: 1.3.52
|
||||
'@swc/jest':
|
||||
specifier: ^0.2.26
|
||||
version: 0.2.26(@swc/core@1.3.52)
|
||||
'@testing-library/react':
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@tsconfig/docusaurus':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
'@types/color':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
'@types/jest':
|
||||
specifier: ^29.4.0
|
||||
version: 29.4.0
|
||||
'@types/mdx':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
|
@ -2900,6 +2915,21 @@ importers:
|
|||
i18next-browser-languagedetector:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
identity-obj-proxy:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
jest:
|
||||
specifier: ^29.5.0
|
||||
version: 29.5.0(@types/node@18.11.18)
|
||||
jest-environment-jsdom:
|
||||
specifier: ^29.0.0
|
||||
version: 29.2.2
|
||||
jest-transform-stub:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
jest-transformer-svg:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(jest@29.5.0)(react@18.2.0)
|
||||
just-kebab-case:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
|
@ -6762,26 +6792,6 @@ packages:
|
|||
jest-mock: 27.5.1
|
||||
dev: true
|
||||
|
||||
/@jest/environment@29.2.2:
|
||||
resolution: {integrity: sha512-OWn+Vhu0I1yxuGBJEFFekMYc8aGBGrY4rt47SOh/IFaI+D7ZHCk7pKRiSoZ2/Ml7b0Ony3ydmEHRx/tEOC7H1A==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/fake-timers': 29.4.1
|
||||
'@jest/types': 29.5.0
|
||||
'@types/node': 18.11.18
|
||||
jest-mock: 29.4.1
|
||||
dev: true
|
||||
|
||||
/@jest/environment@29.4.1:
|
||||
resolution: {integrity: sha512-pJ14dHGSQke7Q3mkL/UZR9ZtTOxqskZaC91NzamEH4dlKRt42W+maRBXiw/LWkdJe+P0f/zDR37+SPMplMRlPg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/fake-timers': 29.4.1
|
||||
'@jest/types': 29.5.0
|
||||
'@types/node': 18.11.18
|
||||
jest-mock: 29.4.1
|
||||
dev: true
|
||||
|
||||
/@jest/environment@29.5.0:
|
||||
resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -6821,30 +6831,6 @@ packages:
|
|||
jest-util: 27.5.1
|
||||
dev: true
|
||||
|
||||
/@jest/fake-timers@29.2.2:
|
||||
resolution: {integrity: sha512-nqaW3y2aSyZDl7zQ7t1XogsxeavNpH6kkdq+EpXncIDvAkjvFD7hmhcIs1nWloengEWUoWqkqSA6MSbf9w6DgA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@sinonjs/fake-timers': 9.1.2
|
||||
'@types/node': 18.11.18
|
||||
jest-message-util: 29.4.1
|
||||
jest-mock: 29.4.1
|
||||
jest-util: 29.4.1
|
||||
dev: true
|
||||
|
||||
/@jest/fake-timers@29.4.1:
|
||||
resolution: {integrity: sha512-/1joI6rfHFmmm39JxNfmNAO3Nwm6Y0VoL5fJDy7H1AtWrD1CgRtqJbN9Ld6rhAkGO76qqp4cwhhxJ9o9kYjQMw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@sinonjs/fake-timers': 10.0.2
|
||||
'@types/node': 18.11.18
|
||||
jest-message-util: 29.5.0
|
||||
jest-mock: 29.4.1
|
||||
jest-util: 29.5.0
|
||||
dev: true
|
||||
|
||||
/@jest/fake-timers@29.5.0:
|
||||
resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -14021,13 +14007,13 @@ packages:
|
|||
canvas:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@jest/environment': 29.2.2
|
||||
'@jest/fake-timers': 29.2.2
|
||||
'@jest/environment': 29.5.0
|
||||
'@jest/fake-timers': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@types/jsdom': 20.0.0
|
||||
'@types/node': 18.11.18
|
||||
jest-mock: 29.2.2
|
||||
jest-util: 29.2.1
|
||||
jest-mock: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
jsdom: 20.0.2
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
|
@ -14051,11 +14037,11 @@ packages:
|
|||
resolution: {integrity: sha512-x/H2kdVgxSkxWAIlIh9MfMuBa0hZySmfsC5lCsWmWr6tZySP44ediRKDUiNggX/eHLH7Cd5ZN10Rw+XF5tXsqg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/environment': 29.4.1
|
||||
'@jest/fake-timers': 29.4.1
|
||||
'@jest/environment': 29.5.0
|
||||
'@jest/fake-timers': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
'@types/node': 18.11.18
|
||||
jest-mock: 29.4.1
|
||||
jest-mock: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
dev: true
|
||||
|
||||
|
@ -14146,21 +14132,6 @@ packages:
|
|||
stack-utils: 2.0.5
|
||||
dev: true
|
||||
|
||||
/jest-message-util@29.4.1:
|
||||
resolution: {integrity: sha512-H4/I0cXUaLeCw6FM+i4AwCnOwHRgitdaUFOdm49022YD5nfyr8C/DrbXOBEyJaj+w/y0gGJ57klssOaUiLLQGQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.18.6
|
||||
'@jest/types': 29.5.0
|
||||
'@types/stack-utils': 2.0.1
|
||||
chalk: 4.1.2
|
||||
graceful-fs: 4.2.10
|
||||
micromatch: 4.0.5
|
||||
pretty-format: 29.5.0
|
||||
slash: 3.0.0
|
||||
stack-utils: 2.0.5
|
||||
dev: true
|
||||
|
||||
/jest-message-util@29.5.0:
|
||||
resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -14184,24 +14155,6 @@ packages:
|
|||
'@types/node': 18.11.18
|
||||
dev: true
|
||||
|
||||
/jest-mock@29.2.2:
|
||||
resolution: {integrity: sha512-1leySQxNAnivvbcx0sCB37itu8f4OX2S/+gxLAV4Z62shT4r4dTG9tACDywUAEZoLSr36aYUTsVp3WKwWt4PMQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@types/node': 18.11.18
|
||||
jest-util: 29.4.1
|
||||
dev: true
|
||||
|
||||
/jest-mock@29.4.1:
|
||||
resolution: {integrity: sha512-MwA4hQ7zBOcgVCVnsM8TzaFLVUD/pFWTfbkY953Y81L5ret3GFRZtmPmRFAjKQSdCKoJvvqOu6Bvfpqlwwb0dQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@types/node': 18.11.18
|
||||
jest-util: 29.5.0
|
||||
dev: true
|
||||
|
||||
/jest-mock@29.5.0:
|
||||
resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -14383,30 +14336,6 @@ packages:
|
|||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/jest-util@29.2.1:
|
||||
resolution: {integrity: sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@types/node': 18.11.18
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.5.0
|
||||
graceful-fs: 4.2.10
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/jest-util@29.4.1:
|
||||
resolution: {integrity: sha512-bQy9FPGxVutgpN4VRc0hk6w7Hx/m6L53QxpDreTZgJd9gfx/AV2MjyPde9tGyZRINAUrSv57p2inGBu2dRLmkQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@jest/types': 29.5.0
|
||||
'@types/node': 18.11.18
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
graceful-fs: 4.2.10
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/jest-util@29.5.0:
|
||||
resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
|
Loading…
Reference in a new issue