feat: add i18n

This commit is contained in:
Sergey Krylov 2024-07-09 07:27:06 +03:00
parent 0e16b4f70a
commit 3516914ad3
43 changed files with 413 additions and 113 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -38,12 +38,16 @@
"@mui/material": "5.15.19",
"@react-spring/web": "9.7.3",
"classnames": "2.5.1",
"i18next": "23.11.5",
"i18next-browser-languagedetector": "8.0.0",
"i18next-http-backend": "2.5.2",
"notistack": "3.0.1",
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.3.1",
"react-i18next": "14.1.2",
"react-router-dom": "6.23.1",
"workbox-core": "7.1.0",
"workbox-precaching": "7.1.0",

View File

@ -0,0 +1,4 @@
{
"title": "Analyze Page",
"toolbar": "Analyze Page Toolbar"
}

View File

@ -0,0 +1,5 @@
{
"title": "Something goes wrong",
"toolbar": "Unhandled error",
"404-toolbar": "Page does not exist"
}

View File

@ -0,0 +1,4 @@
{
"title": "Payments Page",
"toolbar": "Payments Page Toolbar"
}

View File

@ -0,0 +1,4 @@
{
"title": "Schedule Page",
"toolbar": "Schedule Page Toolbar"
}

View File

@ -0,0 +1,5 @@
{
"analyze-page-link": "Analyze",
"payments-page-link": "Payments list",
"schedule-page-link": "Schedule"
}

View File

@ -0,0 +1,3 @@
{
"language": "English"
}

View File

@ -0,0 +1,4 @@
{
"title": "Аналитика",
"toolbar": "Доска аналитики"
}

View File

@ -0,0 +1,5 @@
{
"title": "Что-то пошло не так",
"toolbar": "Произошла непредвиденная ошибка",
"404-toolbar": "Такой страницы не существует"
}

View File

@ -0,0 +1,4 @@
{
"title": "Платежи",
"toolbar": "Доска платежей"
}

View File

@ -0,0 +1,4 @@
{
"title": "График платежей",
"toolbar": "Доска графика платежей"
}

View File

@ -0,0 +1,5 @@
{
"analyze-page-link": "Аналитика",
"payments-page-link": "Список платежей",
"schedule-page-link": "График платежей"
}

View File

@ -0,0 +1,3 @@
{
"language": "Русский"
}

View File

@ -1,8 +1,8 @@
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
import PieChartIcon from '@mui/icons-material/PieChart';
import CircularProgress from '@mui/material/CircularProgress';
import { Suspense } from 'react';
import { useTranslation } from 'react-i18next';
import { createBrowserRouter, Navigate, RouterProvider as RRDRouterProvider } from 'react-router-dom';
import { AnalyzePage, AnalyzePageToolbar } from 'pages/AnalyzePage';
@ -11,6 +11,7 @@ import { PaymentsPage, PaymentsPageToolbar } from 'pages/PaymentsPage';
import { SchedulePage, SchedulePageToolbar } from 'pages/SchedulePage';
import { AsideMenuLayout } from 'widgets/AsideMenuLayout';
import { PageLoader } from 'widgets/PageLoader';
import type { ReactNode } from 'react';
import type { RouteObject } from 'react-router-dom';
@ -23,76 +24,78 @@ type PageListItem = {
Toolbar?: ReactNode;
};
const pages: PageListItem[] = [
{
icon: <PieChartIcon />,
title: 'Аналитика',
path: '/analyze',
Content: (
<Suspense fallback={<CircularProgress />}>
<AnalyzePage />
</Suspense>
),
Toolbar: <AnalyzePageToolbar />,
},
{
icon: <CalendarMonthIcon />,
title: 'График платежей',
path: '/schedule',
Content: (
<Suspense fallback={<CircularProgress />}>
<SchedulePage />
</Suspense>
),
Toolbar: <SchedulePageToolbar />,
},
{
icon: <FormatListBulletedIcon />,
title: 'Список платежей',
path: '/payments',
Content: (
<Suspense fallback={<CircularProgress />}>
<PaymentsPage />
</Suspense>
),
Toolbar: <PaymentsPageToolbar />,
},
];
const RouterProvider = () => {
const { t } = useTranslation('sidebar');
const routes: RouteObject[] = pages.map((page) => ({
path: page.path,
element: (
const pages: PageListItem[] = [
{
icon: <PieChartIcon />,
title: t('analyze-page-link'),
path: '/analyze',
Content: <AnalyzePage />,
Toolbar: <AnalyzePageToolbar />,
},
{
icon: <CalendarMonthIcon />,
title: t('schedule-page-link'),
path: '/schedule',
Content: <SchedulePage />,
Toolbar: <SchedulePageToolbar />,
},
{
icon: <FormatListBulletedIcon />,
title: t('payments-page-link'),
path: '/payments',
Content: <PaymentsPage />,
Toolbar: <PaymentsPageToolbar />,
},
];
const Fallback = (
<AsideMenuLayout
Content={page.Content}
Content={<PageLoader />}
pages={pages}
Toolbar={page.Toolbar}
/>
),
errorElement: (
<AsideMenuLayout
Content={<ErrorPage />}
pages={pages}
Toolbar={<ErrorPageToolbar />}
/>
),
}));
const router = createBrowserRouter([
...routes,
{
path: '/',
element: <Navigate replace to="/analyze" />,
);
const routes: RouteObject[] = pages.map((page) => ({
path: page.path,
element: (
<Suspense fallback={Fallback}>
<AsideMenuLayout
Content={page.Content}
pages={pages}
Toolbar={page.Toolbar}
/>
</Suspense>
),
errorElement: (
<AsideMenuLayout
Content={<ErrorPage type="404" />}
pages={pages}
Toolbar={<ErrorPageToolbar type="404" />}
/>
<Suspense fallback={Fallback}>
<AsideMenuLayout
Content={<ErrorPage />}
pages={pages}
Toolbar={<ErrorPageToolbar />}
/>
</Suspense>
),
},
]);
}));
const RouterProvider = () => (
<RRDRouterProvider router={router} />
);
const router = createBrowserRouter([
...routes,
{
path: '/',
element: <Navigate replace to="/analyze" />,
errorElement: (
<AsideMenuLayout
Content={<ErrorPage type="404" />}
pages={pages}
Toolbar={<ErrorPageToolbar type="404" />}
/>
),
},
]);
return (
<RRDRouterProvider router={router} />
);
};
export default RouterProvider;

View File

@ -0,0 +1 @@
export { default as LangSwitcher } from './ui/LangSwitcher.ui';

View File

@ -0,0 +1,30 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import i18next from 'i18n';
import { LANGUAGE } from 'shared/constants';
import type { FC, ChangeEventHandler } from 'react';
const LangSwitcher: FC = () => {
const { i18n } = useTranslation();
const onChangedHandler = useCallback<ChangeEventHandler<HTMLSelectElement>>(async (e) => {
await i18next.changeLanguage(e.target.value);
}, []);
return (
<select value={i18n.language} onChange={onChangedHandler}>
{Object.entries(LANGUAGE).map(([lang, params]) => (
<option key={lang} value={lang}>
{params.emoji}
{params.name}
</option>
))}
</select>
);
};
export default LangSwitcher;

22
src/i18n.ts Normal file
View File

@ -0,0 +1,22 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
import type { HttpBackendOptions } from 'i18next-http-backend';
// eslint-disable-next-line @typescript-eslint/no-floating-promises
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init<HttpBackendOptions>({
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
load: 'currentOnly',
backend: {
loadPath: 'locales/{{lng}}/{{ns}}.json',
},
});
export default i18n;

View File

@ -1,3 +1,4 @@
import './i18n';
import 'shared/styles/fonts/fonts.css';
import React from 'react';
import { createRoot } from 'react-dom/client';

View File

@ -1,10 +1,16 @@
import { useTranslation } from 'react-i18next';
import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react';
type AnalyzePageProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
const AnalyzePage: FC<AnalyzePageProps> = (props) => (
<div {...props}>
Analyze page
</div>
);
const AnalyzePage: FC<AnalyzePageProps> = (props) => {
const { t } = useTranslation('analyze-page');
return (
<div {...props}>
{t('title')}
</div>
);
};
export default AnalyzePage;

View File

@ -1,15 +1,20 @@
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import type { ToolbarProps } from '@mui/material/Toolbar/Toolbar';
import type { FC } from 'react';
type AnalyzePageToolbarProps = ToolbarProps;
const AnalyzePageToolbar: FC<AnalyzePageToolbarProps> = (props) => (
<Toolbar {...props}>
<Typography noWrap component="div" variant="h6">
Analyze Page Toolbar
</Typography>
</Toolbar>
);
const AnalyzePageToolbar: FC<AnalyzePageToolbarProps> = (props) => {
const { t } = useTranslation('analyze-page');
return (
<Toolbar {...props}>
<Typography noWrap component="div" variant="h6">
{t('toolbar')}
</Typography>
</Toolbar>
);
};
export default AnalyzePageToolbar;

View File

@ -1,5 +1,6 @@
import { Grid } from '@mui/material';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import _404Image from 'public/assets/images/404.png';
import OopsImage from 'public/assets/images/oops.png';
@ -16,6 +17,8 @@ const ErrorPage: FC<ErrorPageProps> = (props) => {
...restProps
} = props;
const { t } = useTranslation('error-page');
if (type === 'ErrorBoundary') {
return (
<Grid
@ -28,7 +31,7 @@ const ErrorPage: FC<ErrorPageProps> = (props) => {
>
<Grid item>
<Typography variant="h5">
Что-то пошло не так
{t('title')}
</Typography>
</Grid>

View File

@ -1,5 +1,6 @@
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import type { ToolbarProps } from '@mui/material/Toolbar/Toolbar';
import type { FC } from 'react';
@ -8,17 +9,19 @@ export type ErrorPageToolbarProps = ToolbarProps & {
type?: '404' | 'ErrorBoundary';
};
const TITLE: Record<NonNullable<ErrorPageToolbarProps['type']>, string> = {
404: 'Страницы не существует',
ErrorBoundary: 'Непредвиденная ошибка',
};
const ErrorPageToolbar: FC<ErrorPageToolbarProps> = (props) => {
const {
type = 'ErrorBoundary',
...restProps
} = props;
const { t } = useTranslation('error-page');
const TITLE: Record<NonNullable<ErrorPageToolbarProps['type']>, string> = {
404: t('404-toolbar'),
ErrorBoundary: t('toolbar'),
};
return (
<Toolbar {...restProps}>
<Typography noWrap component="div" variant="h6">

View File

@ -1,10 +1,16 @@
import { useTranslation } from 'react-i18next';
import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react';
export type PaymentsPageProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
const PaymentsPage: FC<PaymentsPageProps> = (props) => (
<div {...props}>
Payments Page
</div>
);
const PaymentsPage: FC<PaymentsPageProps> = (props) => {
const { t } = useTranslation('payments-page');
return (
<div {...props}>
{t('title')}
</div>
);
};
export default PaymentsPage;

View File

@ -1,16 +1,21 @@
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import type { ToolbarProps } from '@mui/material/Toolbar/Toolbar';
import type { FC } from 'react';
export type PaymentsPageToolbarProps = ToolbarProps;
const PaymentsPageToolbar: FC<PaymentsPageToolbarProps> = (props) => (
<Toolbar {...props}>
<Typography noWrap component="div" variant="h6">
Payments Page Toolbar
</Typography>
</Toolbar>
);
const PaymentsPageToolbar: FC<PaymentsPageToolbarProps> = (props) => {
const { t } = useTranslation('payments-page');
return (
<Toolbar {...props}>
<Typography noWrap component="div" variant="h6">
{t('toolbar')}
</Typography>
</Toolbar>
);
};
export default PaymentsPageToolbar;

View File

@ -1,10 +1,16 @@
import { useTranslation } from 'react-i18next';
import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react';
export type SchedulePageProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
const SchedulePage: FC<SchedulePageProps> = (props) => (
<div {...props}>
Schedule Page
</div>
);
const SchedulePage: FC<SchedulePageProps> = (props) => {
const { t } = useTranslation('schedule-page');
return (
<div {...props}>
{t('title')}
</div>
);
};
export default SchedulePage;

View File

@ -1,16 +1,21 @@
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import type { ToolbarProps } from '@mui/material/Toolbar/Toolbar';
import type { FC } from 'react';
type SchedulePageToolbarProps = ToolbarProps;
const SchedulePageToolbar: FC<SchedulePageToolbarProps> = (props) => (
<Toolbar {...props}>
<Typography noWrap component="div" variant="h6">
Schedule Page Toolbar
</Typography>
</Toolbar>
);
const SchedulePageToolbar: FC<SchedulePageToolbarProps> = (props) => {
const { t } = useTranslation('schedule-page');
return (
<Toolbar {...props}>
<Typography noWrap component="div" variant="h6">
{t('toolbar')}
</Typography>
</Toolbar>
);
};
export default SchedulePageToolbar;

View File

@ -1,5 +1,5 @@
export { DAYS_IN_WEEK, MONTHS, WEEK_DAYS } from './date';
export { MONTHS_LABEL, WEEK_DAYS_LABEL } from './locale';
export { MONTHS_LABEL, WEEK_DAYS_LABEL, LANGUAGE } from './locale';
export type {
MonthsKeys,
MonthsValues,

View File

@ -136,3 +136,14 @@ export const WEEK_DAYS_LABEL: Record<WeekDaysValues, WeekDaysLabelLocale> = {
},
},
};
export const LANGUAGE = {
ru: {
name: 'Русский',
emoji: '🇷🇺',
},
en: {
name: 'English',
emoji: '🇬🇧',
},
};

View File

@ -7,6 +7,8 @@ import { animated, useTransition } from '@react-spring/web';
import { useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { LangSwitcher } from 'features/LangSwitcher';
import { DesktopAppBar, MobileAppBar } from './components/AppBar';
import { AsideMenuContext, AsideMenuProvider } from './components/AsideMenuProvider';
import { DesktopDrawer } from './components/DesktopDrawer';
@ -15,6 +17,7 @@ import { MobileDrawer } from './components/MobileDrawer';
import type { MenuItemProps } from './components/MenuItem/ui/MenuItem.ui';
import type { BoxProps } from '@mui/material/Box/Box';
// eslint-disable-next-line import/max-dependencies
import type { FC, ReactNode } from 'react';
export const DESKTOP_DRAWER_WIDTH = 250;
@ -32,7 +35,6 @@ const AsideMenuLayout: FC<AsideMenuLayoutProps> = (props) => {
Toolbar,
...restProps
} = props;
const { desktopDrawerOpen, setMobileDrawerOpen } = useContext(AsideMenuContext);
const theme = useTheme();
const isMobileView = useMediaQuery(theme.breakpoints.down('sm'));
@ -49,7 +51,7 @@ const AsideMenuLayout: FC<AsideMenuLayoutProps> = (props) => {
const AnimatedContent = (
<>
{transitions((styles) => (
<animated.div style={styles}>
<animated.div style={{ ...styles, height: '100%' }}>
{Content}
</animated.div>
))}
@ -85,6 +87,8 @@ const AsideMenuLayout: FC<AsideMenuLayoutProps> = (props) => {
</MobileDrawer>
{AnimatedToolBar}
<LangSwitcher />
</MobileAppBar>
)
}
@ -113,14 +117,16 @@ const AsideMenuLayout: FC<AsideMenuLayoutProps> = (props) => {
</List>
</DesktopDrawer>
<Box sx={{ flexGrow: 1 }}>
<Box sx={{ flexGrow: 1, height: '100vh' }}>
{ Boolean(Toolbar) && (
<DesktopAppBar position="static">
{AnimatedToolBar}
<LangSwitcher />
</DesktopAppBar>
)}
<Box component="main" sx={{ p: 3 }}>
<Box component="main" sx={{ p: 3, height: '100%' }}>
{AnimatedContent}
</Box>
</Box>

View File

@ -0,0 +1 @@
export { default as PageLoader } from './ui/PageLoader.ui';

View File

@ -0,0 +1,20 @@
import CircularProgress from '@mui/material/CircularProgress';
import { styled } from '@mui/material/styles';
const CenteredDiv = styled('div')(
() => ({
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}),
);
const PageLoader = () => (
<CenteredDiv>
<CircularProgress />
</CenteredDiv>
);
export default PageLoader;

114
yarn.lock
View File

@ -2671,7 +2671,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.11.2":
"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.23.2":
version: 7.24.7
resolution: "@babel/runtime@npm:7.24.7"
dependencies:
@ -7769,6 +7769,15 @@ __metadata:
languageName: node
linkType: hard
"cross-fetch@npm:4.0.0":
version: 4.0.0
resolution: "cross-fetch@npm:4.0.0"
dependencies:
node-fetch: "npm:^2.6.12"
checksum: 10c0/386727dc4c6b044746086aced959ff21101abb85c43df5e1d151547ccb6f338f86dec3f28b9dbddfa8ff5b9ec8662ed2263ad4607a93b2dc354fb7fe3bbb898a
languageName: node
linkType: hard
"cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3":
version: 7.0.3
resolution: "cross-spawn@npm:7.0.3"
@ -10579,6 +10588,15 @@ __metadata:
languageName: node
linkType: hard
"html-parse-stringify@npm:^3.0.1":
version: 3.0.1
resolution: "html-parse-stringify@npm:3.0.1"
dependencies:
void-elements: "npm:3.1.0"
checksum: 10c0/159292753d48b84d216d61121054ae5a33466b3db5b446e2ffc093ac077a411a99ce6cbe0d18e55b87cf25fa3c5a86c4d8b130b9719ec9b66623259000c72c15
languageName: node
linkType: hard
"html-webpack-plugin@npm:5.6.0":
version: 5.6.0
resolution: "html-webpack-plugin@npm:5.6.0"
@ -10751,6 +10769,33 @@ __metadata:
languageName: node
linkType: hard
"i18next-browser-languagedetector@npm:8.0.0":
version: 8.0.0
resolution: "i18next-browser-languagedetector@npm:8.0.0"
dependencies:
"@babel/runtime": "npm:^7.23.2"
checksum: 10c0/08a7c747ec18a0743b54390a0b42836d814b15fb49c92b90c259298e6a6e7e001ff9df99d0fd308283a34fceea53b4d9053aa1695a37f696bc04160017bbb524
languageName: node
linkType: hard
"i18next-http-backend@npm:2.5.2":
version: 2.5.2
resolution: "i18next-http-backend@npm:2.5.2"
dependencies:
cross-fetch: "npm:4.0.0"
checksum: 10c0/57cfbeeea40a751134e1521fac89b14b780c9ad87b74a4f7d7168a88a8b603efed9f5df455dddbb408f7e268eae5aa0a6888e47b9f10cb9482d812d4d6997ffa
languageName: node
linkType: hard
"i18next@npm:23.11.5":
version: 23.11.5
resolution: "i18next@npm:23.11.5"
dependencies:
"@babel/runtime": "npm:^7.23.2"
checksum: 10c0/b0bec64250a3e529d4c51e2fc511406a85c5dde3d005d3aabe919551ca31dfc0a8f5490bf6e44649822e895a1fa91a58092d112367669cd11b2eb89e6ba90d1a
languageName: node
linkType: hard
"iconv-lite@npm:0.4.24":
version: 0.4.24
resolution: "iconv-lite@npm:0.4.24"
@ -13039,6 +13084,9 @@ __metadata:
eslint-config-ksv741: "npm:0.0.3"
eslint-import-resolver-webpack: "npm:0.13.8"
husky: "npm:9.0.11"
i18next: "npm:23.11.5"
i18next-browser-languagedetector: "npm:8.0.0"
i18next-http-backend: "npm:2.5.2"
identity-obj-proxy: "npm:3.0.0"
jest: "npm:29.7.0"
jest-environment-jsdom: "npm:29.7.0"
@ -13050,6 +13098,7 @@ __metadata:
react-dnd: "npm:16.0.1"
react-dnd-html5-backend: "npm:16.0.1"
react-dom: "npm:18.3.1"
react-i18next: "npm:14.1.2"
react-router-dom: "npm:6.23.1"
standard-version: "npm:9.5.0"
ts-jest: "npm:29.1.4"
@ -13156,6 +13205,20 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^2.6.12":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
whatwg-url: "npm:^5.0.0"
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8
languageName: node
linkType: hard
"node-forge@npm:^1":
version: 1.3.1
resolution: "node-forge@npm:1.3.1"
@ -14857,6 +14920,24 @@ __metadata:
languageName: node
linkType: hard
"react-i18next@npm:14.1.2":
version: 14.1.2
resolution: "react-i18next@npm:14.1.2"
dependencies:
"@babel/runtime": "npm:^7.23.9"
html-parse-stringify: "npm:^3.0.1"
peerDependencies:
i18next: ">= 23.2.3"
react: ">= 16.8.0"
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: 10c0/cb8a83b3696639f083dc9f770d9d9e0681c0fe56f6b5fe24cd6facce08d363c37bd3440078e9d63abacabd7037b783e6b4e4d0c935de9c8dda7820bd4ef7e329
languageName: node
linkType: hard
"react-is@npm:^16.13.1, react-is@npm:^16.7.0":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
@ -16587,6 +16668,13 @@ __metadata:
languageName: node
linkType: hard
"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11
languageName: node
linkType: hard
"trim-newlines@npm:^3.0.0":
version: 3.0.1
resolution: "trim-newlines@npm:3.0.1"
@ -17134,6 +17222,13 @@ __metadata:
languageName: node
linkType: hard
"void-elements@npm:3.1.0":
version: 3.1.0
resolution: "void-elements@npm:3.1.0"
checksum: 10c0/0b8686f9f9aa44012e9bd5eabf287ae0cde409b9a2854c5a2335cb83920c957668ac5876e3f0d158dd424744ac411a7270e64128556b451ed3bec875ef18534d
languageName: node
linkType: hard
"w3c-xmlserializer@npm:^4.0.0":
version: 4.0.0
resolution: "w3c-xmlserializer@npm:4.0.0"
@ -17171,6 +17266,13 @@ __metadata:
languageName: node
linkType: hard
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"
checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db
languageName: node
linkType: hard
"webidl-conversions@npm:^4.0.2":
version: 4.0.2
resolution: "webidl-conversions@npm:4.0.2"
@ -17415,6 +17517,16 @@ __metadata:
languageName: node
linkType: hard
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"
dependencies:
tr46: "npm:~0.0.3"
webidl-conversions: "npm:^3.0.0"
checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5
languageName: node
linkType: hard
"whatwg-url@npm:^7.0.0":
version: 7.1.0
resolution: "whatwg-url@npm:7.1.0"