diff --git a/apps/web/src/components/directories/directories-doh-grid.tsx b/apps/web/src/components/directories/directories-doh-grid.tsx
index ded0d79..5d8bbe0 100644
--- a/apps/web/src/components/directories/directories-doh-grid.tsx
+++ b/apps/web/src/components/directories/directories-doh-grid.tsx
@@ -15,7 +15,7 @@ import {
import type { DohProfile } from '@/types/api'
const filterFields: FilterField[] = [
- createSearchFilterField('search', 'Поиск', 'Поиск DoH профилей…'),
+ createSearchFilterField('search', 'Поиск', 'Поиск DoH-профилей…'),
]
export function DirectoriesDohGrid({
@@ -81,7 +81,7 @@ export function DirectoriesDohGrid({
return (
)
}
diff --git a/apps/web/src/components/lookup/lookup-add-step.tsx b/apps/web/src/components/lookup/lookup-add-step.tsx
index 2b21a1f..845a107 100644
--- a/apps/web/src/components/lookup/lookup-add-step.tsx
+++ b/apps/web/src/components/lookup/lookup-add-step.tsx
@@ -20,6 +20,7 @@ import {
import { CascaderInput, CascaderValue } from '@/components/reui/cascader/cascader-nav'
import type { CascaderNode } from '@/components/reui/cascader/cascader-types'
import { ApiError, apiMutate } from '@/lib/api-client'
+import { CASCADER_LABELS_RU } from '@/lib/reui-i18n-ru'
import type {
BgpCommunity,
LookupQueryKind,
@@ -202,9 +203,7 @@ export function LookupAddStep({
selectable={allowEmptyCommunity ? 'any' : 'leaf'}
mode="columns"
labels={{
- search: 'Поиск',
- empty: 'Ничего не найдено',
- back: 'Назад',
+ ...CASCADER_LABELS_RU,
rootLevel: 'Модули',
}}
>
diff --git a/apps/web/src/components/network/speaker-form-dialog.tsx b/apps/web/src/components/network/speaker-form-dialog.tsx
index 4955354..df86ec1 100644
--- a/apps/web/src/components/network/speaker-form-dialog.tsx
+++ b/apps/web/src/components/network/speaker-form-dialog.tsx
@@ -185,7 +185,7 @@ export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps
label="Роль"
items={[
{ value: 'replica', label: 'Реплика' },
- { value: 'master', label: 'Мастер (плоскость)' },
+ { value: 'master', label: 'Основной' },
]}
value={role}
onValueChange={(v) => setRole(v ?? 'replica')}
diff --git a/apps/web/src/components/operations/operations-jobs-kanban.tsx b/apps/web/src/components/operations/operations-jobs-kanban.tsx
index 22274bd..84cf8aa 100644
--- a/apps/web/src/components/operations/operations-jobs-kanban.tsx
+++ b/apps/web/src/components/operations/operations-jobs-kanban.tsx
@@ -31,7 +31,7 @@ import type { JobRow } from '@/types/api'
const COLUMN_ORDER = ['queued', 'running', 'succeeded', 'failed'] as const
const COLUMN_LABELS: Record<(typeof COLUMN_ORDER)[number], string> = {
- queued: 'Очередь',
+ queued: 'В очереди',
running: 'Выполняются',
succeeded: 'Успешные',
failed: 'Ошибки',
diff --git a/apps/web/src/components/reui-kit/frame-data-grid.tsx b/apps/web/src/components/reui-kit/frame-data-grid.tsx
index 762015b..11320e4 100644
--- a/apps/web/src/components/reui-kit/frame-data-grid.tsx
+++ b/apps/web/src/components/reui-kit/frame-data-grid.tsx
@@ -47,7 +47,7 @@ import {
FrameTitle,
} from '@/components/reui/frame'
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 = ColumnDef
@@ -298,6 +298,7 @@ function FrameDataGridBody({
recordCount={data.length}
onRowClick={onRowClick}
emptyMessage={emptyTitle}
+ i18n={DATA_GRID_I18N_RU}
tableLayout={kitDataGridTableLayout({
dense,
width: tableWidth,
@@ -317,7 +318,7 @@ function FrameDataGridBody({
<>
-
+
>
) : null}
diff --git a/apps/web/src/components/settings/session-settings-tab.tsx b/apps/web/src/components/settings/session-settings-tab.tsx
index 9b41e0c..0468365 100644
--- a/apps/web/src/components/settings/session-settings-tab.tsx
+++ b/apps/web/src/components/settings/session-settings-tab.tsx
@@ -18,7 +18,7 @@ import {
} from '@evobgp/ui/components/item'
const ROLE_LABELS: Record = {
- viewer: 'Просмотр',
+ viewer: 'Наблюдатель',
editor: 'Редактор',
operator: 'Оператор',
node: 'Нода',
diff --git a/apps/web/src/lib/filters-i18n.ts b/apps/web/src/lib/filters-i18n.ts
index 4d60fb9..e797553 100644
--- a/apps/web/src/lib/filters-i18n.ts
+++ b/apps/web/src/lib/filters-i18n.ts
@@ -53,6 +53,46 @@ export const FILTERS_LABELS_RU: Partial = {
noValue: 'нет значения',
selectCondition: 'Выберите условие',
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 = {
diff --git a/apps/web/src/lib/metrics/module-type-breakdown.ts b/apps/web/src/lib/metrics/module-type-breakdown.ts
index cbf08b1..09b3430 100644
--- a/apps/web/src/lib/metrics/module-type-breakdown.ts
+++ b/apps/web/src/lib/metrics/module-type-breakdown.ts
@@ -3,7 +3,7 @@ import type { ModuleRow, ModuleType } from '@/types/api'
import type { BreakdownSlice } from './types'
const TYPE_META: Record = {
- 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)' },
CDN_CIDRS: { label: 'CDN', color: 'var(--color-chart-3)' },
IP_RANGES: { label: 'IP-диапазоны', color: 'var(--color-chart-4)' },
diff --git a/apps/web/src/lib/reui-i18n-ru.ts b/apps/web/src/lib/reui-i18n-ru.ts
new file mode 100644
index 0000000..37bfffc
--- /dev/null
+++ b/apps/web/src/lib/reui-i18n-ru.ts
@@ -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}` на
+ * (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: 'Поиск…',
+}
diff --git a/apps/web/src/lib/ui-labels.ts b/apps/web/src/lib/ui-labels.ts
index 92904fa..a10728e 100644
--- a/apps/web/src/lib/ui-labels.ts
+++ b/apps/web/src/lib/ui-labels.ts
@@ -16,7 +16,7 @@ export function dohPolicyRu(policy: DohResolverPolicy | string | null | undefine
export function moduleTypeRu(type: string): string {
switch (type) {
case 'AS_PREFIXES':
- return 'AS (номера)'
+ return 'AS-префиксы'
case 'CDN_CIDRS':
return 'CDN CIDR'
case 'DOMAINS':
@@ -30,7 +30,7 @@ export function moduleTypeRu(type: string): string {
const JOB_KIND_RU: Record = {
module_refresh: 'Обновление модуля',
- tenant_refresh: 'Обновление тенанта',
+ tenant_refresh: 'Обновление арендатора',
peer_reconcile: 'Согласование пиров',
deploy_apply: 'Применение на спикеры',
apply: 'Применение конфигурации',
@@ -60,7 +60,7 @@ export function jobKindRu(kind: string): string {
const JOB_TRIGGER_RU: Record = {
scheduler: 'по расписанию',
- api: 'вручную',
+ api: 'через API',
peer_patch: 'правка пиров',
}
@@ -129,7 +129,7 @@ const JOB_STATUS_RU: Record = {
disabled: 'Выключен',
archived: 'В архиве',
block: 'Блокировать',
- accept: 'Принимать',
+ accept: 'Принять',
}
export function jobStatusRu(status: string): string {
diff --git a/apps/web/src/routes/_auth/directories.tsx b/apps/web/src/routes/_auth/directories.tsx
index a1282c1..ab7eb30 100644
--- a/apps/web/src/routes/_auth/directories.tsx
+++ b/apps/web/src/routes/_auth/directories.tsx
@@ -55,7 +55,7 @@ function DirectoriesComponent() {
),
},
{
- label: 'DoH профили',
+ label: 'DoH-профили',
value: dohProfiles.length,
icon: ,
footer: (
@@ -137,7 +137,7 @@ function DirectoriesComponent() {
defaultValue="communities"
items={[
{ 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' },
]}
>