fix(web): localize reui grids and filters, unify ru terminology
Русификация переведена на официальные i18n-слои ReUI: полный русский словарь DataGrid (lib/reui-i18n-ru.ts) подключён через data-grid-i18n во FrameDataGrid и ResourcePage — меню сортировки, «Загрузка…», «Нет данных» и пагинация больше не откатываются к английским дефолтам компонентов. Словарь фильтров дополнен ~30 недостающими ключами, каскадер в мастере проверки переведён целиком. Исправлены некорректные и разъехавшиеся лейблы: accept -> «Принять», API-триггер -> «через API», «арендатор» вместо «Тенант», «Наблюдатель» вместо «Просмотр», «Основной» вместо «Мастер (плоскость)», «AS-префиксы», «DoH-профили», колонка канбана «В очереди».
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
|||||||
import type { DohProfile } from '@/types/api'
|
import type { DohProfile } from '@/types/api'
|
||||||
|
|
||||||
const filterFields: FilterField[] = [
|
const filterFields: FilterField[] = [
|
||||||
createSearchFilterField('search', 'Поиск', 'Поиск DoH профилей…'),
|
createSearchFilterField('search', 'Поиск', 'Поиск DoH-профилей…'),
|
||||||
]
|
]
|
||||||
|
|
||||||
export function DirectoriesDohGrid({
|
export function DirectoriesDohGrid({
|
||||||
@@ -81,7 +81,7 @@ export function DirectoriesDohGrid({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ResourcePage
|
<ResourcePage
|
||||||
title="DoH профили"
|
title="DoH-профили"
|
||||||
description="Резолверы DNS-over-HTTPS для доменных модулей"
|
description="Резолверы DNS-over-HTTPS для доменных модулей"
|
||||||
filterFields={filterFields}
|
filterFields={filterFields}
|
||||||
filterQuery={filterQuery}
|
filterQuery={filterQuery}
|
||||||
@@ -94,7 +94,7 @@ export function DirectoriesDohGrid({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
primaryAction={actions}
|
primaryAction={actions}
|
||||||
pinLastColumn={Boolean(canWrite && onEdit)}
|
pinLastColumn={Boolean(canWrite && onEdit)}
|
||||||
emptyState={{ title: 'Нет DoH профилей', action: actions }}
|
emptyState={{ title: 'Нет DoH-профилей', action: actions }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { CascaderInput, CascaderValue } from '@/components/reui/cascader/cascader-nav'
|
import { CascaderInput, CascaderValue } from '@/components/reui/cascader/cascader-nav'
|
||||||
import type { CascaderNode } from '@/components/reui/cascader/cascader-types'
|
import type { CascaderNode } from '@/components/reui/cascader/cascader-types'
|
||||||
import { ApiError, apiMutate } from '@/lib/api-client'
|
import { ApiError, apiMutate } from '@/lib/api-client'
|
||||||
|
import { CASCADER_LABELS_RU } from '@/lib/reui-i18n-ru'
|
||||||
import type {
|
import type {
|
||||||
BgpCommunity,
|
BgpCommunity,
|
||||||
LookupQueryKind,
|
LookupQueryKind,
|
||||||
@@ -202,9 +203,7 @@ export function LookupAddStep({
|
|||||||
selectable={allowEmptyCommunity ? 'any' : 'leaf'}
|
selectable={allowEmptyCommunity ? 'any' : 'leaf'}
|
||||||
mode="columns"
|
mode="columns"
|
||||||
labels={{
|
labels={{
|
||||||
search: 'Поиск',
|
...CASCADER_LABELS_RU,
|
||||||
empty: 'Ничего не найдено',
|
|
||||||
back: 'Назад',
|
|
||||||
rootLevel: 'Модули',
|
rootLevel: 'Модули',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
|
|||||||
label="Роль"
|
label="Роль"
|
||||||
items={[
|
items={[
|
||||||
{ value: 'replica', label: 'Реплика' },
|
{ value: 'replica', label: 'Реплика' },
|
||||||
{ value: 'master', label: 'Мастер (плоскость)' },
|
{ value: 'master', label: 'Основной' },
|
||||||
]}
|
]}
|
||||||
value={role}
|
value={role}
|
||||||
onValueChange={(v) => setRole(v ?? 'replica')}
|
onValueChange={(v) => setRole(v ?? 'replica')}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import type { JobRow } from '@/types/api'
|
|||||||
const COLUMN_ORDER = ['queued', 'running', 'succeeded', 'failed'] as const
|
const COLUMN_ORDER = ['queued', 'running', 'succeeded', 'failed'] as const
|
||||||
|
|
||||||
const COLUMN_LABELS: Record<(typeof COLUMN_ORDER)[number], string> = {
|
const COLUMN_LABELS: Record<(typeof COLUMN_ORDER)[number], string> = {
|
||||||
queued: 'Очередь',
|
queued: 'В очереди',
|
||||||
running: 'Выполняются',
|
running: 'Выполняются',
|
||||||
succeeded: 'Успешные',
|
succeeded: 'Успешные',
|
||||||
failed: 'Ошибки',
|
failed: 'Ошибки',
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import {
|
|||||||
FrameTitle,
|
FrameTitle,
|
||||||
} from '@/components/reui/frame'
|
} from '@/components/reui/frame'
|
||||||
import { EmptyState } from '@/components/empty-state'
|
import { EmptyState } from '@/components/empty-state'
|
||||||
import { DATA_GRID_PAGINATION_RU } from '@/lib/data-grid-defaults'
|
import { DATA_GRID_I18N_RU } from '@/lib/reui-i18n-ru'
|
||||||
|
|
||||||
export type DataGridColumnDef<TData extends object> = ColumnDef<DataGridFeatures, TData>
|
export type DataGridColumnDef<TData extends object> = ColumnDef<DataGridFeatures, TData>
|
||||||
|
|
||||||
@@ -298,6 +298,7 @@ function FrameDataGridBody<TData extends object>({
|
|||||||
recordCount={data.length}
|
recordCount={data.length}
|
||||||
onRowClick={onRowClick}
|
onRowClick={onRowClick}
|
||||||
emptyMessage={emptyTitle}
|
emptyMessage={emptyTitle}
|
||||||
|
i18n={DATA_GRID_I18N_RU}
|
||||||
tableLayout={kitDataGridTableLayout({
|
tableLayout={kitDataGridTableLayout({
|
||||||
dense,
|
dense,
|
||||||
width: tableWidth,
|
width: tableWidth,
|
||||||
@@ -317,7 +318,7 @@ function FrameDataGridBody<TData extends object>({
|
|||||||
<>
|
<>
|
||||||
<Separator />
|
<Separator />
|
||||||
<FrameFooter>
|
<FrameFooter>
|
||||||
<DataGridPagination {...DATA_GRID_PAGINATION_RU} />
|
<DataGridPagination sizes={[10, 25, 50]} />
|
||||||
</FrameFooter>
|
</FrameFooter>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from '@evobgp/ui/components/item'
|
} from '@evobgp/ui/components/item'
|
||||||
|
|
||||||
const ROLE_LABELS: Record<string, string> = {
|
const ROLE_LABELS: Record<string, string> = {
|
||||||
viewer: 'Просмотр',
|
viewer: 'Наблюдатель',
|
||||||
editor: 'Редактор',
|
editor: 'Редактор',
|
||||||
operator: 'Оператор',
|
operator: 'Оператор',
|
||||||
node: 'Нода',
|
node: 'Нода',
|
||||||
|
|||||||
@@ -53,6 +53,46 @@ export const FILTERS_LABELS_RU: Partial<FilterLabels> = {
|
|||||||
noValue: 'нет значения',
|
noValue: 'нет значения',
|
||||||
selectCondition: 'Выберите условие',
|
selectCondition: 'Выберите условие',
|
||||||
incomplete: 'незавершённый фильтр',
|
incomplete: 'незавершённый фильтр',
|
||||||
|
branchAffordance: 'открывает список',
|
||||||
|
exclusiveHint: 'нельзя сочетать с другими вариантами',
|
||||||
|
exclusiveAnnouncement: (label, cleared) =>
|
||||||
|
cleared === 1
|
||||||
|
? `Выбрано «${label}». 1 другой вариант сброшен.`
|
||||||
|
: `Выбрано «${label}». Других вариантов сброшено: ${cleared}.`,
|
||||||
|
itemCount: (count) => `Элементов: ${count}`,
|
||||||
|
fieldsLabel: 'Поля',
|
||||||
|
resultsAnnouncement: (count) =>
|
||||||
|
count === 1 ? '1 результат' : `Результатов: ${count}`,
|
||||||
|
actionsLabel: 'Действия',
|
||||||
|
rowLabel: (condition, depth) => `${condition}, уровень ${depth}`,
|
||||||
|
groupLabel: (description, depth) => `${description}, уровень ${depth}`,
|
||||||
|
groupAnnouncement: (added) => (added ? 'Группа добавлена' : 'Группа удалена'),
|
||||||
|
reorderAnnouncement: (label, position, total) =>
|
||||||
|
`${label} перемещён на позицию ${position} из ${total}`,
|
||||||
|
moveAnnouncement: (label, destination, position, total) =>
|
||||||
|
`${label} перемещён в ${destination}, позиция ${position} из ${total}`,
|
||||||
|
combinatorLabel: (word) => `${word}, изменить связку`,
|
||||||
|
chipMenu: (fieldLabel) => `Параметры фильтра «${fieldLabel}»`,
|
||||||
|
stepAnnouncement: (step, label) => {
|
||||||
|
if (step === 'field') return `Выберите поле. ${label}`
|
||||||
|
if (step === 'operator') return `Выберите условие для ${label}`
|
||||||
|
return `Введите значение для ${label}`
|
||||||
|
},
|
||||||
|
countAnnouncement: (count) =>
|
||||||
|
count === 1 ? 'Применён 1 фильтр' : `Применено фильтров: ${count}`,
|
||||||
|
valueCount: (count) => `Выбрано: ${count}`,
|
||||||
|
valueRange: (from, to) => `${from} – ${to}`,
|
||||||
|
rangeFrom: (fieldLabel) => `${fieldLabel} от`,
|
||||||
|
rangeTo: (fieldLabel) => `${fieldLabel} до`,
|
||||||
|
rangeSeparator: '—',
|
||||||
|
negated: (operatorLabel) => `не ${operatorLabel}`,
|
||||||
|
issueOperator: 'Выберите условие',
|
||||||
|
issueValue: 'Введите значение',
|
||||||
|
issueRange: 'Заполните оба конца диапазона',
|
||||||
|
issueRangeOrder: 'Конец диапазона раньше его начала',
|
||||||
|
issueEmptyGroup: 'В этой группе пока нет условий',
|
||||||
|
issueSummary: (count) =>
|
||||||
|
count === 1 ? '1 строка требует внимания' : `Строк требует внимания: ${count}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FILTERS_OPERATOR_LABELS_RU: FilterOperatorLabels = {
|
export const FILTERS_OPERATOR_LABELS_RU: FilterOperatorLabels = {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { ModuleRow, ModuleType } from '@/types/api'
|
|||||||
import type { BreakdownSlice } from './types'
|
import type { BreakdownSlice } from './types'
|
||||||
|
|
||||||
const TYPE_META: Record<ModuleType, { label: string; color: string }> = {
|
const TYPE_META: Record<ModuleType, { label: string; color: string }> = {
|
||||||
AS_PREFIXES: { label: 'AS / префиксы', color: 'var(--color-chart-1)' },
|
AS_PREFIXES: { label: 'AS-префиксы', color: 'var(--color-chart-1)' },
|
||||||
DOMAINS: { label: 'Домены', color: 'var(--color-chart-2)' },
|
DOMAINS: { label: 'Домены', color: 'var(--color-chart-2)' },
|
||||||
CDN_CIDRS: { label: 'CDN', color: 'var(--color-chart-3)' },
|
CDN_CIDRS: { label: 'CDN', color: 'var(--color-chart-3)' },
|
||||||
IP_RANGES: { label: 'IP-диапазоны', color: 'var(--color-chart-4)' },
|
IP_RANGES: { label: 'IP-диапазоны', color: 'var(--color-chart-4)' },
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import type {
|
||||||
|
DataGridI18nLabels,
|
||||||
|
DataGridI18nOverrides,
|
||||||
|
} from '@/components/reui/data-grid/data-grid-i18n'
|
||||||
|
import type { CascaderLabels } from '@/components/reui/cascader/cascader-types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Русские формы множественного числа: 1 элемент, 2 элемента, 5 элементов.
|
||||||
|
*/
|
||||||
|
function ruPlural(count: number, one: string, few: string, many: string): string {
|
||||||
|
const mod10 = count % 10
|
||||||
|
const mod100 = count % 100
|
||||||
|
if (mod10 === 1 && mod100 !== 11) return one
|
||||||
|
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return few
|
||||||
|
return many
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Единый русский словарь ReUI DataGrid — официальный i18n-слой (@reui/data-grid-i18n).
|
||||||
|
* Подключается один раз через `i18n={DATA_GRID_I18N_RU}` на <DataGrid>
|
||||||
|
* (FrameDataGrid и ResourcePage) — наследуют все гриды приложения.
|
||||||
|
* Полнота словаря проверяется компилятором: тип DataGridI18nLabels требует все ключи.
|
||||||
|
*
|
||||||
|
* @see https://reui.io/docs/components/base/data-grid
|
||||||
|
*/
|
||||||
|
const DATA_GRID_LABELS_RU: DataGridI18nLabels = {
|
||||||
|
/* Меню заголовка столбца. */
|
||||||
|
sortAscending: 'По возрастанию',
|
||||||
|
sortDescending: 'По убыванию',
|
||||||
|
pinColumnStart: 'Закрепить слева',
|
||||||
|
pinColumnEnd: 'Закрепить справа',
|
||||||
|
moveColumnStart: 'Сдвинуть влево',
|
||||||
|
moveColumnEnd: 'Сдвинуть вправо',
|
||||||
|
columnsMenu: 'Столбцы',
|
||||||
|
unpinColumn: (title) => `Открепить столбец «${title}»`,
|
||||||
|
toggleColumns: 'Настройка столбцов',
|
||||||
|
/* Афордансы строк и ячеек. */
|
||||||
|
rowCreate: 'Добавить строку',
|
||||||
|
pinRow: 'Закрепить строку',
|
||||||
|
unpinRow: 'Открепить строку',
|
||||||
|
selectRow: 'Выбрать строку',
|
||||||
|
selectAll: 'Выбрать всё',
|
||||||
|
expandRow: 'Развернуть строку',
|
||||||
|
collapseRow: 'Свернуть строку',
|
||||||
|
dragToReorder: 'Перетащите, чтобы изменить порядок',
|
||||||
|
dragToReorderRow: 'Перетащите, чтобы изменить порядок строк',
|
||||||
|
reorderingUnavailable: 'Изменение порядка недоступно',
|
||||||
|
/* Состояния грида. */
|
||||||
|
loading: 'Загрузка…',
|
||||||
|
empty: 'Нет данных',
|
||||||
|
allRowsLoaded: 'Все записи загружены',
|
||||||
|
/* Пагинация. */
|
||||||
|
rowsPerPage: 'Строк на странице',
|
||||||
|
paginationInfo: ({ from, to, count }) => `${from}–${to} из ${count}`,
|
||||||
|
previousPage: 'Предыдущая страница',
|
||||||
|
nextPage: 'Следующая страница',
|
||||||
|
goToPage: (page) => `Перейти на страницу ${page}`,
|
||||||
|
paginationEllipsis: '…',
|
||||||
|
/* Фасетный фильтр столбца. */
|
||||||
|
filterSelectedCount: (count) => `Выбрано: ${count}`,
|
||||||
|
filterNoResults: 'Ничего не найдено.',
|
||||||
|
filterClear: 'Сбросить фильтры',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DATA_GRID_I18N_RU: DataGridI18nOverrides = {
|
||||||
|
labels: DATA_GRID_LABELS_RU,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Единый русский словарь ReUI Cascader (@reui/cascader-i18n).
|
||||||
|
* Передаётся в `labels` каскадера; поверх — контекстный `rootLevel`.
|
||||||
|
*
|
||||||
|
* @see https://reui.io/docs/components/base/cascader
|
||||||
|
*/
|
||||||
|
const CASCADER_ROOT_LEVEL_RU = 'Корневой уровень'
|
||||||
|
|
||||||
|
const cascaderItemCount = (count: number) =>
|
||||||
|
`${count} ${ruPlural(count, 'элемент', 'элемента', 'элементов')}`
|
||||||
|
|
||||||
|
export const CASCADER_LABELS_RU: CascaderLabels = {
|
||||||
|
search: (parentLabel) =>
|
||||||
|
parentLabel ? `Поиск: ${parentLabel}…` : 'Поиск…',
|
||||||
|
back: 'Назад',
|
||||||
|
loading: 'Загрузка…',
|
||||||
|
loadingMore: 'Загрузка…',
|
||||||
|
loadMore: 'Ещё',
|
||||||
|
error: 'Не удалось загрузить элементы.',
|
||||||
|
retry: 'Повторить',
|
||||||
|
empty: 'Ничего не найдено.',
|
||||||
|
selectedCount: (count) => `Выбрано: ${count}`,
|
||||||
|
breadcrumbLabel: 'Навигация',
|
||||||
|
chipsLabel: 'Выбранные элементы',
|
||||||
|
removeChip: (label) => `Удалить «${label}»`,
|
||||||
|
pathSeparator: '/',
|
||||||
|
rootLevel: CASCADER_ROOT_LEVEL_RU,
|
||||||
|
itemCount: cascaderItemCount,
|
||||||
|
branchAffordance: 'подменю',
|
||||||
|
selectedState: 'выбрано',
|
||||||
|
partiallySelectedState: 'частично выбрано',
|
||||||
|
columnsLabel: 'Уровни',
|
||||||
|
actionsLabel: 'Действия',
|
||||||
|
submenuAffordance: 'открывает меню',
|
||||||
|
panelLabel: 'Варианты',
|
||||||
|
keyboardHint: (mode, dir) => {
|
||||||
|
const open = dir === 'rtl' ? 'влево' : 'вправо'
|
||||||
|
const back = dir === 'rtl' ? 'вправо' : 'влево'
|
||||||
|
if (mode === 'tree') {
|
||||||
|
return `Стрелка ${open} — раскрыть уровень, стрелка ${back} — свернуть.`
|
||||||
|
}
|
||||||
|
if (mode === 'columns') {
|
||||||
|
return `Стрелка ${open} — открыть следующий столбец, стрелка ${back} — вернуться.`
|
||||||
|
}
|
||||||
|
return `Стрелка ${open} — открыть ветку, стрелка ${back} — вернуться.`
|
||||||
|
},
|
||||||
|
rootAnnouncement: (count) => `${CASCADER_ROOT_LEVEL_RU}, ${cascaderItemCount(count)}`,
|
||||||
|
expandedAnnouncement: (label, count) => `${label} раскрыт, ${cascaderItemCount(count)}`,
|
||||||
|
collapsedAnnouncement: (label) => `${label} свёрнут`,
|
||||||
|
levelAnnouncement: (parentLabel, depth, count) =>
|
||||||
|
`${parentLabel}, уровень ${depth}, ${cascaderItemCount(count)}`,
|
||||||
|
resultsAnnouncement: (count) =>
|
||||||
|
count === 1 ? '1 результат' : `Результатов: ${count}`,
|
||||||
|
maxReachedAnnouncement: (max) => `Достигнут предел выбора: ${max}`,
|
||||||
|
cascadeAnnouncement: (label, count, selecting) =>
|
||||||
|
`${label} ${selecting ? 'выбрано' : 'снято'}, вложенных затронуто: ${count}`,
|
||||||
|
searchingAnnouncement: 'Поиск…',
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ export function dohPolicyRu(policy: DohResolverPolicy | string | null | undefine
|
|||||||
export function moduleTypeRu(type: string): string {
|
export function moduleTypeRu(type: string): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'AS_PREFIXES':
|
case 'AS_PREFIXES':
|
||||||
return 'AS (номера)'
|
return 'AS-префиксы'
|
||||||
case 'CDN_CIDRS':
|
case 'CDN_CIDRS':
|
||||||
return 'CDN CIDR'
|
return 'CDN CIDR'
|
||||||
case 'DOMAINS':
|
case 'DOMAINS':
|
||||||
@@ -30,7 +30,7 @@ export function moduleTypeRu(type: string): string {
|
|||||||
|
|
||||||
const JOB_KIND_RU: Record<string, string> = {
|
const JOB_KIND_RU: Record<string, string> = {
|
||||||
module_refresh: 'Обновление модуля',
|
module_refresh: 'Обновление модуля',
|
||||||
tenant_refresh: 'Обновление тенанта',
|
tenant_refresh: 'Обновление арендатора',
|
||||||
peer_reconcile: 'Согласование пиров',
|
peer_reconcile: 'Согласование пиров',
|
||||||
deploy_apply: 'Применение на спикеры',
|
deploy_apply: 'Применение на спикеры',
|
||||||
apply: 'Применение конфигурации',
|
apply: 'Применение конфигурации',
|
||||||
@@ -60,7 +60,7 @@ export function jobKindRu(kind: string): string {
|
|||||||
|
|
||||||
const JOB_TRIGGER_RU: Record<string, string> = {
|
const JOB_TRIGGER_RU: Record<string, string> = {
|
||||||
scheduler: 'по расписанию',
|
scheduler: 'по расписанию',
|
||||||
api: 'вручную',
|
api: 'через API',
|
||||||
peer_patch: 'правка пиров',
|
peer_patch: 'правка пиров',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ const JOB_STATUS_RU: Record<string, string> = {
|
|||||||
disabled: 'Выключен',
|
disabled: 'Выключен',
|
||||||
archived: 'В архиве',
|
archived: 'В архиве',
|
||||||
block: 'Блокировать',
|
block: 'Блокировать',
|
||||||
accept: 'Принимать',
|
accept: 'Принять',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function jobStatusRu(status: string): string {
|
export function jobStatusRu(status: string): string {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function DirectoriesComponent() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'DoH профили',
|
label: 'DoH-профили',
|
||||||
value: dohProfiles.length,
|
value: dohProfiles.length,
|
||||||
icon: <Globe className="size-4" />,
|
icon: <Globe className="size-4" />,
|
||||||
footer: (
|
footer: (
|
||||||
@@ -137,7 +137,7 @@ function DirectoriesComponent() {
|
|||||||
defaultValue="communities"
|
defaultValue="communities"
|
||||||
items={[
|
items={[
|
||||||
{ value: 'communities', label: 'BGP community', count: communities.length },
|
{ value: 'communities', label: 'BGP community', count: communities.length },
|
||||||
{ value: 'doh', label: 'DoH профили', count: dohProfiles.length, badgeVariant: 'info-light' },
|
{ value: 'doh', label: 'DoH-профили', count: dohProfiles.length, badgeVariant: 'info-light' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TabsContent value="communities" className="mt-0">
|
<TabsContent value="communities" className="mt-0">
|
||||||
|
|||||||
Reference in New Issue
Block a user