Files
mdm-ordinis/ordinis-admin-ui/src/i18n.ts
T
Zimin A.N. 4252ad93a8 feat(records): bulk export CSV выбранных + pagination
Backend POST /api/v1/dictionaries/{name}/records/export-selected.csv:
принимает businessKeys[] (1..500), возвращает CSV с колонками
business_key, valid_from, valid_to, scope, data (JSONB inline).
Не-найденные ключи отдаются строкой с пустыми ячейками — admin видит
что ключ обработан, но данных нет. RFC 4180 экранирование.

requireReadAccess: для экспорта достаточно прав чтения (не write).

POST а не GET потому что >100 ключей не помещается в URL params
(Server: Request URI Too Long).

Frontend useBulkExportRecords: axios POST с responseType: blob →
temporary <a download> link → клик → revokeObjectURL через rAF.
Filename берётся из Content-Disposition header.

Toolbar: новая кнопка "Экспорт CSV" (variant=secondary) рядом с
"Закрыть выбранные" (variant=danger). Loading state через isPending.

Pagination для records list (PAGE_SIZE=50, client-side):
- visibleRecords = filteredRecords.slice(...)
- Footer "Записи 1–50 из 100, Стр. 1 из 2" с Prev/Next кнопками
- Auto-reset на page=0 при изменении filter/scope/AOI
- selectAll/visibleKeys теперь = только current page (WYSIWYG)
- При >50 записей видно навигацию, при <=50 footer скрыт

Прогон: 20/20 e2e + 60/60 rest-api + 89/89 admin-ui.
2026-05-07 12:12:31 +03:00

589 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'ru-RU',
supportedLngs: ['ru-RU', 'en-US'],
interpolation: { escapeValue: false },
resources: {
'ru-RU': {
translation: {
'app.title': 'Ordinis НСИ ЦУОД',
'nav.dictionaries': 'Справочники',
'nav.audit': 'Аудит',
'nav.outbox': 'Outbox',
'nav.webhooks': 'Webhooks',
'webhooks.title': 'Webhook subscriptions',
'webhooks.subtitle': 'HTTP-доставка событий не-Kafka подписчикам',
'webhooks.empty': 'Нет подписок. Создайте первую через кнопку справа.',
'webhooks.active': 'Активна',
'webhooks.inactive': 'Отключена',
'webhooks.allScopes': 'все scope',
'webhooks.allDicts': 'все справочники',
'webhooks.allEvents': 'все события',
'webhooks.notFound': 'Подписка не найдена',
'webhooks.col.name': 'Имя',
'webhooks.col.url': 'URL',
'webhooks.col.scopes': 'Scope filter',
'webhooks.col.dictionaries': 'Справочники',
'webhooks.col.status': 'Статус',
'webhooks.action.create': 'Создать',
'webhooks.action.delete': 'Удалить',
'webhooks.action.rotateSecret': 'Сменить secret',
'webhooks.action.test': 'Тест ping',
'webhooks.test.success': 'Receiver получил test event и ответил 2xx',
'webhooks.test.failure': 'Receiver не принял test event',
'webhooks.test.rejected': 'Запрос не отправлен (SSRF guard / serialization)',
'webhooks.test.latency': 'latency',
'webhooks.confirmDelete': 'Удалить подписку «{{name}}»? Все pending deliveries удалятся каскадом.',
'webhooks.confirmRotate': 'Сменить HMAC secret? Старый сразу перестанет работать — receiver надо обновить заранее.',
'webhooks.field.name': 'Имя подписки',
'webhooks.field.url': 'Webhook URL',
'webhooks.field.urlHint': 'https:// или http://. Private CIDR (10/8, 192.168/16, 127/8) запрещены SSRF guard',
'webhooks.field.scopeFilter': 'Scope filter',
'webhooks.field.scopeFilterHint': 'Пусто = все scope',
'webhooks.field.dictionaryFilter': 'Справочники (CSV)',
'webhooks.field.dictionaryFilterHint': 'Через запятую: spacecraft, satellite_type. Пусто = все',
'webhooks.field.eventTypeFilter': 'Типы событий (CSV)',
'webhooks.field.eventTypeFilterHint': 'record.created, record.updated, record.closed. Пусто = все',
'webhooks.field.description': 'Описание',
'webhooks.field.createdBy': 'Создатель',
'webhooks.error.nameUrlRequired': 'Имя и URL обязательны',
'webhooks.error.urlScheme': 'URL должен начинаться с http:// или https://',
'webhooks.secret.title': 'HMAC secret для «{{name}}»',
'webhooks.secret.warningTitle': 'Скопируй сейчас!',
'webhooks.secret.warning': 'Этот secret показывается ОДИН раз. Дальше будет замаскирован. Передай receiver чтобы он мог проверять X-Ordinis-Signature header.',
'webhooks.secret.rotatedTitle': 'Secret обновлён',
'webhooks.secret.rotatedWarning': 'Новый secret. Старый перестал работать. Обнови receiver.',
'webhooks.secret.copy': 'Копировать',
'webhooks.secret.copied': 'Скопировано',
'webhooks.secret.acknowledge': 'Сохранил',
'webhooks.deliveries.title': 'История доставок',
'webhooks.deliveries.empty': 'Доставок ещё не было',
'webhooks.deliveries.col.eventId': 'Event ID',
'webhooks.deliveries.col.attempts': 'Попыток',
'webhooks.deliveries.col.lastAttempt': 'Последняя попытка',
'webhooks.deliveries.col.lastStatusCode': 'HTTP статус',
'webhooks.deliveries.col.lastError': 'Последняя ошибка',
'webhooks.deliveries.action.retry': 'Повторить доставку',
'webhooks.deliveries.confirmRetry': 'Сбросить attemptCount=0 и поставить в очередь повтор? Receiver получит payload снова.',
'webhooks.deliveries.filter.label': 'Фильтр по статусу',
'webhooks.deliveries.filter.clear': 'Сбросить',
'audit.title': 'Журнал аудита',
'audit.subtitle': 'Юридически значимый лог изменений НСИ',
'audit.filter.dictionary': 'Справочник',
'audit.filter.user': 'Пользователь',
'audit.filter.action': 'Действие',
'audit.filter.businessKey': 'Бизнес-ключ',
'audit.filter.from': 'С',
'audit.filter.to': 'По',
'audit.filter.reset': 'Сбросить',
'audit.filter.apply': 'Применить',
'audit.filter.active': 'Активные фильтры',
'audit.filter.removeChip': 'Убрать фильтр',
'audit.col.time': 'Время',
'audit.col.action': 'Действие',
'audit.col.dictionary': 'Справочник',
'audit.col.businessKey': 'Бизнес-ключ',
'audit.col.user': 'Пользователь',
'audit.col.scope': 'Scope',
'audit.col.trace': 'Trace',
'audit.col.diff': 'Изменения',
'audit.action.expand': 'Раскрыть',
'audit.action.export': 'Экспорт CSV',
'audit.empty': 'Нет записей по выбранным фильтрам',
'audit.action.CREATE': 'Создание',
'audit.action.UPDATE': 'Изменение',
'audit.action.CLOSE': 'Закрытие',
'audit.diff.before': 'Было',
'audit.diff.after': 'Стало',
'audit.page.of': 'Стр. {{cur}} из {{total}}',
'audit.page.size': 'На странице',
'audit.page.prev': 'Назад',
'audit.page.next': 'Вперёд',
'outbox.title': 'Outbox events',
'outbox.subtitle': 'Очередь публикации в Kafka и DLQ',
'outbox.stats.pending': 'В очереди',
'outbox.stats.dlq': 'В DLQ',
'outbox.dlq.col.id': 'ID',
'outbox.dlq.col.eventType': 'Тип',
'outbox.dlq.col.dictionary': 'Справочник',
'outbox.dlq.col.aggregateId': 'Aggregate ID',
'outbox.dlq.col.topic': 'Topic',
'outbox.dlq.col.attempts': 'Попыток',
'outbox.dlq.col.lastError': 'Последняя ошибка',
'outbox.dlq.col.dlqAt': 'В DLQ с',
'outbox.dlq.empty': 'DLQ пуста — все события публикуются успешно',
'header.scope': 'Доступный scope',
'dict.list.subtitle': 'Каталоги НСИ ЦУОД ОДХ',
'dict.list.search.placeholder': 'Поиск по названию, коду или описанию',
'dict.list.search.empty': 'Ничего не найдено',
'dict.list.found': 'Показано {{shown}} из {{total}}',
'dict.list.recordCount_one': '{{count}} запись',
'dict.list.recordCount_few': '{{count}} записи',
'dict.list.recordCount_many': '{{count}} записей',
'dict.list.recordCount_other': '{{count}} записей',
'dict.list.section.PUBLIC': 'Публичные',
'dict.list.section.INTERNAL': 'Внутренние',
'dict.list.section.RESTRICTED': 'Ограниченные',
'dict.empty': 'В этом справочнике пока нет записей',
'dict.col.businessKey': 'Бизнес-ключ',
'dict.col.scope': 'Scope',
'dict.col.validFrom': 'Действует с',
'dict.col.locale': 'Локаль',
'dict.col.actions': 'Действия',
'dict.list.records': 'записей',
'dict.action.create': 'Создать запись',
'dict.action.edit': 'Изменить',
'dict.action.close': 'Закрыть',
'dict.confirmClose.title': 'Закрыть запись?',
'dict.confirmClose.body':
'Запись «{{key}}» будет помечена как закрытая (valid_to = now). Действие обратимо через создание новой версии.',
'dict.confirmClose.reason': 'Причина (опционально)',
'dict.confirmClose.reasonPlaceholder': 'например, «дубликат» или «выведено из эксплуатации»',
'dict.bulk.selectAll': 'Выбрать все на странице',
'dict.bulk.selectRow': 'Выбрать запись',
'dict.bulk.selectedCount_one': 'Выбрано {{count}} из {{total}}',
'dict.bulk.selectedCount_few': 'Выбрано {{count}} из {{total}}',
'dict.bulk.selectedCount_many': 'Выбрано {{count}} из {{total}}',
'dict.bulk.selectedCount_other': 'Выбрано {{count}} из {{total}}',
'dict.bulk.clearSelection': 'Снять выделение',
'dict.bulk.closeAction': 'Закрыть выбранные',
'dict.bulk.exportAction': 'Экспорт CSV',
'dict.page.prev': 'Назад',
'dict.page.next': 'Вперёд',
'dict.page.of': 'Стр. {{cur}} из {{total}}',
'dict.page.range': 'Записи {{from}}{{to}} из {{total}}',
'dict.bulk.confirmTitle': 'Массовое закрытие записей',
'dict.bulk.confirmBody':
'{{count}} запис(ей) будут помечены как закрытые (valid_to = now). Действие обратимо через создание новой версии. Каждая запись закрывается отдельно — если часть уже закрыта, остальные обработаются.',
'dict.bulk.confirmAction': 'Закрыть {{count}}',
'dict.bulk.resultClosed': 'Закрыто: {{count}}',
'dict.bulk.resultSkipped': 'Пропущено (уже закрыто или не найдено): {{count}}',
'dict.bulk.resultErrors': 'Ошибок: {{count}}',
'dict.bulk.dismiss': 'Готово',
'form.identity': 'Идентификация',
'form.idSourceNote': 'Бизнес-ключ создастся автоматически из поля «{{field}}»',
'form.tabs.identity': 'Идентификация',
'form.tabs.description': 'Описание',
'form.tabs.extra': 'Дополнительно',
'form.businessKey': 'Бизнес-ключ',
'form.validFrom': 'Действует с',
'form.validFromHint': 'Когда запись становится активной. Пусто = с момента создания.',
'form.validTo': 'Действует до',
'form.validToHint': 'Когда перестаёт действовать. Пусто = бессрочно.',
'form.error.validToBeforeFrom': 'Дата окончания должна быть позже даты начала.',
'form.dataFields': 'Поля записи',
'form.cancel': 'Отмена',
'form.save': 'Сохранить',
'form.saving': 'Сохранение...',
'form.error.required': 'Обязательное поле',
'scope.PUBLIC': 'PUBLIC — публичный',
'scope.INTERNAL': 'INTERNAL — внутренний',
'scope.RESTRICTED': 'RESTRICTED — ограниченный',
'schema.action.create': 'Создать справочник',
'schema.action.edit': 'Изменить справочник',
'schema.action.editSchema': 'Схема',
'schema.tabs.metadata': 'Метаданные',
'schema.tabs.schema': 'Поля',
'schema.tabs.preview': 'JSON',
'schema.name': 'Имя',
'schema.nameHint': 'snake_case, начинается с буквы (например my_dictionary)',
'schema.nameInvalid': 'Только snake_case: a-z, 0-9, _; начинается с буквы',
'schema.displayName': 'Отображаемое имя',
'schema.description': 'Описание',
'schema.scope': 'Scope',
'schema.bundle': 'Bundle',
'schema.bundleHint': 'Обычно cuod',
'schema.template.label': 'Создать по шаблону',
'schema.template.hint': 'Скопирует схему, scope и locale выбранного словаря. Имя оставьте уникальное; версия сбросится на 1.0.0.',
'schema.template.placeholder': '— без шаблона —',
'schema.version': 'Версия схемы',
'schema.supportedLocales': 'Поддерживаемые локали',
'schema.defaultLocale': 'Локаль по умолчанию',
'schema.property': 'Поле',
'schema.required': 'Обязательное',
'schema.pattern': 'Pattern (regex)',
'schema.enumValues': 'Значения enum (через запятую)',
'schema.enumValuesHint': 'Например: ACTIVE, INACTIVE, RETIRED',
'schema.arrayEnumOptional': 'Допустимые значения (опц., через запятую)',
'schema.unique': 'Уникальное',
'schema.uniqueHint': 'Дубликаты по этому полю запрещены',
'schema.idSource': 'Использовать как ID (бизнес-ключ)',
'schema.idSourceHint': 'Бизнес-ключ записи будет автоматически = значению этого поля',
'schema.idSourceManual': 'Ввод вручную',
'schema.versionSuggest': 'Рекомендуемая версия: {{v}}',
'schema.diff.blockedTitle': 'Несовместимое изменение схемы',
'schema.diff.blockedBody': 'Эти изменения сломают существующие записи. Сохранение заблокировано. Создайте новый справочник или откатите изменения.',
'schema.diff.compatibleTitle': 'Изменения схемы (совместимые)',
'history.title': 'История версий',
'history.empty': 'История пуста',
'history.current': 'текущая',
'history.active': 'активна',
'history.scheduled': 'запланирована',
'history.closed': 'закрыта',
'history.validFrom': 'действует с',
'history.validTo': 'действует до',
'history.updatedBy': 'кем',
'history.viewData': 'данные записи (JSON)',
'schema.empty': 'Полей нет — добавь первое',
'schema.addProperty': 'Добавить поле',
'schema.delete': 'Удалить',
'schema.moveUp': 'Выше',
'schema.moveDown': 'Ниже',
'schema.kind.label': 'Тип',
'schema.kind.string': 'Строка',
'schema.kind.integer': 'Целое',
'schema.kind.number': 'Число',
'schema.kind.boolean': 'Логическое',
'schema.kind.enum': 'Enum (выбор из списка)',
'schema.kind.reference': 'Ссылка на словарь (FK)',
'schema.kind.localized': 'I18N строка (ru-RU/en-US/...)',
'schema.kind.date': 'Дата',
'schema.kind.datetime': 'Дата+время',
'schema.kind.array_string': 'Массив строк',
'schema.kind.opaque': 'Сложный (JSON)',
'schema.reference.title': 'FK на запись справочника',
'schema.reference.hint': 'Значение поля будет проверяться при сохранении: оно должно совпадать с business-key или указанным полем существующей записи целевого словаря.',
'schema.reference.targetDict': 'Целевой словарь',
'schema.reference.targetField': 'Поле',
'schema.reference.targetFieldHint': 'Обычно code или business_key',
'schema.reference.required': 'Заполните оба: словарь и поле — иначе FK не сработает.',
'schema.opaque.title': 'Сложное поле — редактируется только через JSON-таб',
'schema.opaque.hint': 'Nested object / array of objects. Поля-табе нет редактора, raw schema показан ниже. Меняй через вкладку "JSON".',
'field.name': 'Наименование',
'field.designator': 'Designator (COSPAR)',
'field.norad_id': 'NORAD ID',
'field.status': 'Статус',
'field.country': 'Страна',
'field.operator': 'Оператор',
'field.mass_kg': 'Масса (кг)',
'field.mission': 'Миссия',
'field.orbit': 'Орбита',
'field.launch_date': 'Дата запуска',
'field.decay_date': 'Дата схода',
'field.spectrum_bands': 'Спектральные диапазоны',
'field.satellite_type_code': 'Тип КА (код)',
'field.code': 'Код',
'field.description': 'Описание',
'field.location': 'Местоположение',
'field.coordinates': 'Координаты',
'field.frequency_bands': 'Частотные диапазоны',
'loading': 'Загрузка...',
'error.failed': 'Не удалось загрузить данные',
'auth.login': 'Войти',
'auth.logout': 'Выйти',
'dict.filter.searchPlaceholder': 'Поиск по записям…',
'dict.filter.scopeLabel': 'Фильтр по scope',
'dict.filter.matched': 'Найдено {{count}} из {{total}}',
'dict.filter.clear': 'Сбросить',
'dict.filter.noMatches': 'По фильтру записей не найдено',
'aoi.button': 'AOI фильтр',
'aoi.activeBbox': 'AOI: bbox {{value}}',
'aoi.activePolygon': 'AOI: полигон ({{points}} точек)',
'aoi.clear': 'Сбросить AOI',
'aoi.dialog.title': 'Выбор области интереса (AOI)',
'aoi.dialog.help': 'Нарисуйте прямоугольник или полигон. Применяется как фильтр к записям со spatial geometry.',
'aoi.dialog.apply': 'Применить',
'aoi.dialog.drawRectangle': 'Прямоугольник',
'aoi.dialog.drawPolygon': 'Полигон',
'aoi.dialog.clear': 'Очистить',
'aoi.dialog.polygonHint': 'Клик — добавить точку, двойной клик — закрыть полигон (минимум 3 точки)',
'aoi.dialog.mapAriaLabel': 'Карта для выбора области интереса',
'aoi.error.noShape': 'Сначала нарисуйте область на карте',
'aoi.error.invalid': 'Геометрия некорректна — попробуйте перерисовать',
},
},
'en-US': {
translation: {
'app.title': 'Ordinis MDM',
'nav.dictionaries': 'Dictionaries',
'nav.audit': 'Audit',
'nav.outbox': 'Outbox',
'nav.webhooks': 'Webhooks',
'webhooks.title': 'Webhook subscriptions',
'webhooks.subtitle': 'HTTP delivery of events to non-Kafka consumers',
'webhooks.empty': 'No subscriptions yet. Create one via the button on the right.',
'webhooks.active': 'Active',
'webhooks.inactive': 'Disabled',
'webhooks.allScopes': 'all scopes',
'webhooks.allDicts': 'all dictionaries',
'webhooks.allEvents': 'all events',
'webhooks.notFound': 'Subscription not found',
'webhooks.col.name': 'Name',
'webhooks.col.url': 'URL',
'webhooks.col.scopes': 'Scope filter',
'webhooks.col.dictionaries': 'Dictionaries',
'webhooks.col.status': 'Status',
'webhooks.action.create': 'Create',
'webhooks.action.delete': 'Delete',
'webhooks.action.rotateSecret': 'Rotate secret',
'webhooks.action.test': 'Test ping',
'webhooks.test.success': 'Receiver got the test event and replied 2xx',
'webhooks.test.failure': 'Receiver rejected the test event',
'webhooks.test.rejected': 'Request not sent (SSRF guard / serialization)',
'webhooks.test.latency': 'latency',
'webhooks.confirmDelete': 'Delete subscription "{{name}}"? All pending deliveries will be cascade-deleted.',
'webhooks.confirmRotate': 'Rotate HMAC secret? The old one stops working immediately — update the receiver first.',
'webhooks.field.name': 'Subscription name',
'webhooks.field.url': 'Webhook URL',
'webhooks.field.urlHint': 'https:// or http://. Private CIDRs (10/8, 192.168/16, 127/8) blocked by SSRF guard',
'webhooks.field.scopeFilter': 'Scope filter',
'webhooks.field.scopeFilterHint': 'Empty = all scopes',
'webhooks.field.dictionaryFilter': 'Dictionaries (CSV)',
'webhooks.field.dictionaryFilterHint': 'Comma-separated: spacecraft, satellite_type. Empty = all',
'webhooks.field.eventTypeFilter': 'Event types (CSV)',
'webhooks.field.eventTypeFilterHint': 'record.created, record.updated, record.closed. Empty = all',
'webhooks.field.description': 'Description',
'webhooks.field.createdBy': 'Created by',
'webhooks.error.nameUrlRequired': 'Name and URL are required',
'webhooks.error.urlScheme': 'URL must start with http:// or https://',
'webhooks.secret.title': 'HMAC secret for "{{name}}"',
'webhooks.secret.warningTitle': 'Copy now!',
'webhooks.secret.warning': 'This secret is shown ONCE. It will be masked afterwards. Pass it to the receiver so it can verify the X-Ordinis-Signature header.',
'webhooks.secret.rotatedTitle': 'Secret rotated',
'webhooks.secret.rotatedWarning': 'New secret. Old one stopped working. Update the receiver.',
'webhooks.secret.copy': 'Copy',
'webhooks.secret.copied': 'Copied',
'webhooks.secret.acknowledge': 'Saved it',
'webhooks.deliveries.title': 'Delivery history',
'webhooks.deliveries.empty': 'No deliveries yet',
'webhooks.deliveries.col.eventId': 'Event ID',
'webhooks.deliveries.col.attempts': 'Attempts',
'webhooks.deliveries.col.lastAttempt': 'Last attempt',
'webhooks.deliveries.col.lastStatusCode': 'HTTP status',
'webhooks.deliveries.col.lastError': 'Last error',
'webhooks.deliveries.action.retry': 'Retry delivery',
'webhooks.deliveries.confirmRetry': 'Reset attemptCount=0 and queue a retry? The receiver will get the payload again.',
'webhooks.deliveries.filter.label': 'Status filter',
'webhooks.deliveries.filter.clear': 'Clear',
'audit.title': 'Audit log',
'audit.subtitle': 'Legally significant log of NSI changes',
'audit.filter.dictionary': 'Dictionary',
'audit.filter.user': 'User',
'audit.filter.action': 'Action',
'audit.filter.businessKey': 'Business key',
'audit.filter.from': 'From',
'audit.filter.to': 'To',
'audit.filter.reset': 'Reset',
'audit.filter.apply': 'Apply',
'audit.filter.active': 'Active filters',
'audit.filter.removeChip': 'Remove filter',
'audit.col.time': 'Time',
'audit.col.action': 'Action',
'audit.col.dictionary': 'Dictionary',
'audit.col.businessKey': 'Business key',
'audit.col.user': 'User',
'audit.col.scope': 'Scope',
'audit.col.trace': 'Trace',
'audit.col.diff': 'Diff',
'audit.action.expand': 'Expand',
'audit.action.export': 'Export CSV',
'audit.empty': 'No entries match the filters',
'audit.action.CREATE': 'Create',
'audit.action.UPDATE': 'Update',
'audit.action.CLOSE': 'Close',
'audit.diff.before': 'Before',
'audit.diff.after': 'After',
'audit.page.of': 'Page {{cur}} of {{total}}',
'audit.page.size': 'Per page',
'audit.page.prev': 'Prev',
'audit.page.next': 'Next',
'outbox.title': 'Outbox events',
'outbox.subtitle': 'Publication queue and DLQ',
'outbox.stats.pending': 'Pending',
'outbox.stats.dlq': 'In DLQ',
'outbox.dlq.col.id': 'ID',
'outbox.dlq.col.eventType': 'Type',
'outbox.dlq.col.dictionary': 'Dictionary',
'outbox.dlq.col.aggregateId': 'Aggregate ID',
'outbox.dlq.col.topic': 'Topic',
'outbox.dlq.col.attempts': 'Attempts',
'outbox.dlq.col.lastError': 'Last error',
'outbox.dlq.col.dlqAt': 'DLQ since',
'outbox.dlq.empty': 'DLQ empty — all events publish successfully',
'header.scope': 'Allowed scope',
'dict.list.subtitle': 'Reference data catalogues',
'dict.list.search.placeholder': 'Search by name, code or description',
'dict.list.search.empty': 'Nothing found',
'dict.list.found': 'Showing {{shown}} of {{total}}',
'dict.list.recordCount_one': '{{count}} record',
'dict.list.recordCount_other': '{{count}} records',
'dict.list.section.PUBLIC': 'Public',
'dict.list.section.INTERNAL': 'Internal',
'dict.list.section.RESTRICTED': 'Restricted',
'dict.empty': 'No records in this dictionary yet',
'dict.col.businessKey': 'Business key',
'dict.col.scope': 'Scope',
'dict.col.validFrom': 'Valid from',
'dict.col.locale': 'Locale',
'dict.col.actions': 'Actions',
'dict.list.records': 'records',
'dict.action.create': 'Create record',
'dict.action.edit': 'Edit',
'dict.action.close': 'Close',
'dict.confirmClose.title': 'Close record?',
'dict.confirmClose.body':
'Record "{{key}}" will be marked as closed (valid_to = now). Reversible by creating a new version.',
'dict.confirmClose.reason': 'Reason (optional)',
'dict.confirmClose.reasonPlaceholder': 'e.g., "duplicate" or "decommissioned"',
'dict.bulk.selectAll': 'Select all on page',
'dict.bulk.selectRow': 'Select record',
'dict.bulk.selectedCount_one': '{{count}} of {{total}} selected',
'dict.bulk.selectedCount_other': '{{count}} of {{total}} selected',
'dict.bulk.clearSelection': 'Clear selection',
'dict.bulk.closeAction': 'Close selected',
'dict.bulk.exportAction': 'Export CSV',
'dict.page.prev': 'Prev',
'dict.page.next': 'Next',
'dict.page.of': 'Page {{cur}} of {{total}}',
'dict.page.range': 'Records {{from}}{{to}} of {{total}}',
'dict.bulk.confirmTitle': 'Bulk close records',
'dict.bulk.confirmBody':
'{{count}} record(s) will be marked as closed (valid_to = now). Reversible by creating a new version. Each record is closed individually — if some are already closed, others will still be processed.',
'dict.bulk.confirmAction': 'Close {{count}}',
'dict.bulk.resultClosed': 'Closed: {{count}}',
'dict.bulk.resultSkipped': 'Skipped (already closed or not found): {{count}}',
'dict.bulk.resultErrors': 'Errors: {{count}}',
'dict.bulk.dismiss': 'Done',
'form.identity': 'Identity',
'form.idSourceNote': 'Business key auto-derived from "{{field}}" field',
'form.tabs.identity': 'Identity',
'form.tabs.description': 'Description',
'form.tabs.extra': 'Additional',
'form.businessKey': 'Business key',
'form.validFrom': 'Valid from',
'form.validFromHint': 'When this record becomes active. Empty = from creation moment.',
'form.validTo': 'Valid to',
'form.validToHint': 'When it stops being valid. Empty = forever.',
'form.error.validToBeforeFrom': 'Valid-to must be later than valid-from.',
'form.dataFields': 'Record fields',
'form.cancel': 'Cancel',
'form.save': 'Save',
'form.saving': 'Saving...',
'form.error.required': 'Required field',
'scope.PUBLIC': 'PUBLIC',
'scope.INTERNAL': 'INTERNAL',
'scope.RESTRICTED': 'RESTRICTED',
'schema.action.create': 'Create dictionary',
'schema.action.edit': 'Edit dictionary',
'schema.action.editSchema': 'Schema',
'schema.tabs.metadata': 'Metadata',
'schema.tabs.schema': 'Fields',
'schema.tabs.preview': 'JSON',
'schema.name': 'Name',
'schema.nameHint': 'snake_case, starts with letter (e.g. my_dictionary)',
'schema.nameInvalid': 'Only snake_case: a-z, 0-9, _; must start with letter',
'schema.displayName': 'Display name',
'schema.description': 'Description',
'schema.scope': 'Scope',
'schema.bundle': 'Bundle',
'schema.bundleHint': 'Usually cuod',
'schema.template.label': 'Create from template',
'schema.template.hint': 'Copies schema, scope and locales from the selected dictionary. Use a unique name; version resets to 1.0.0.',
'schema.template.placeholder': '— no template —',
'schema.version': 'Schema version',
'schema.supportedLocales': 'Supported locales',
'schema.defaultLocale': 'Default locale',
'schema.property': 'Field',
'schema.required': 'Required',
'schema.pattern': 'Pattern (regex)',
'schema.enumValues': 'Enum values (comma-separated)',
'schema.enumValuesHint': 'E.g.: ACTIVE, INACTIVE, RETIRED',
'schema.arrayEnumOptional': 'Allowed values (optional, comma-separated)',
'schema.unique': 'Unique',
'schema.uniqueHint': 'Duplicates by this field are not allowed',
'schema.idSource': 'Use as ID (business key)',
'schema.idSourceHint': 'Record business key auto-derived from this field value',
'schema.idSourceManual': 'Manual input',
'schema.versionSuggest': 'Suggested version: {{v}}',
'schema.diff.blockedTitle': 'Breaking schema change',
'schema.diff.blockedBody': 'These changes break existing records. Save blocked. Create a new dictionary or revert.',
'schema.diff.compatibleTitle': 'Schema changes (compatible)',
'history.title': 'Version history',
'history.empty': 'No history',
'history.current': 'current',
'history.active': 'active',
'history.scheduled': 'scheduled',
'history.closed': 'closed',
'history.validFrom': 'valid from',
'history.validTo': 'valid to',
'history.updatedBy': 'by',
'history.viewData': 'record data (JSON)',
'schema.empty': 'No fields yet — add the first one',
'schema.addProperty': 'Add field',
'schema.delete': 'Delete',
'schema.moveUp': 'Move up',
'schema.moveDown': 'Move down',
'schema.kind.label': 'Type',
'schema.kind.string': 'String',
'schema.kind.integer': 'Integer',
'schema.kind.number': 'Number',
'schema.kind.boolean': 'Boolean',
'schema.kind.enum': 'Enum (pick from list)',
'schema.kind.reference': 'Reference to dictionary (FK)',
'schema.kind.localized': 'I18N string (ru-RU/en-US/...)',
'schema.kind.date': 'Date',
'schema.kind.datetime': 'Date+time',
'schema.kind.array_string': 'Array of strings',
'schema.kind.opaque': 'Complex (JSON)',
'schema.opaque.title': 'Complex field — edit only via JSON tab',
'schema.opaque.hint': 'Nested object / array of objects. No editor in Fields tab, raw schema shown below. Edit via JSON tab.',
'schema.reference.title': 'FK to dictionary record',
'schema.reference.hint': 'Value is validated on save: must match the business-key or specified field of an existing record in the target dictionary.',
'schema.reference.targetDict': 'Target dictionary',
'schema.reference.targetField': 'Field',
'schema.reference.targetFieldHint': 'Usually code or business_key',
'schema.reference.required': 'Fill both target dictionary and field — otherwise FK will not work.',
'field.name': 'Name',
'field.designator': 'Designator (COSPAR)',
'field.norad_id': 'NORAD ID',
'field.status': 'Status',
'field.country': 'Country',
'field.operator': 'Operator',
'field.mass_kg': 'Mass (kg)',
'field.mission': 'Mission',
'field.orbit': 'Orbit',
'field.launch_date': 'Launch date',
'field.decay_date': 'Decay date',
'field.spectrum_bands': 'Spectrum bands',
'field.satellite_type_code': 'Satellite type code',
'field.code': 'Code',
'field.description': 'Description',
'field.location': 'Location',
'field.coordinates': 'Coordinates',
'field.frequency_bands': 'Frequency bands',
'loading': 'Loading...',
'error.failed': 'Failed to load data',
'auth.login': 'Sign in',
'auth.logout': 'Sign out',
'dict.filter.searchPlaceholder': 'Search records…',
'dict.filter.scopeLabel': 'Filter by scope',
'dict.filter.matched': 'Matched {{count}} of {{total}}',
'dict.filter.clear': 'Clear',
'dict.filter.noMatches': 'No records match the filter',
'aoi.button': 'AOI filter',
'aoi.activeBbox': 'AOI: bbox {{value}}',
'aoi.activePolygon': 'AOI: polygon ({{points}} points)',
'aoi.clear': 'Clear AOI',
'aoi.dialog.title': 'Select Area of Interest (AOI)',
'aoi.dialog.help': 'Draw a rectangle or polygon. Applied as filter to records with spatial geometry.',
'aoi.dialog.apply': 'Apply',
'aoi.dialog.drawRectangle': 'Rectangle',
'aoi.dialog.drawPolygon': 'Polygon',
'aoi.dialog.clear': 'Clear',
'aoi.dialog.polygonHint': 'Click adds vertex, double-click closes polygon (min 3 vertices)',
'aoi.dialog.mapAriaLabel': 'Map for selecting area of interest',
'aoi.error.noShape': 'Draw an area on the map first',
'aoi.error.invalid': 'Invalid geometry — please redraw',
},
},
},
})
export default i18n