feature: Added Russian language to web admin panel
This commit is contained in:
parent
1292540350
commit
c4dde636e7
3 changed files with 372 additions and 16 deletions
File diff suppressed because one or more lines are too long
24
cmd/telesrv-admin/web/dist/index.html
vendored
24
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -1,13 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>telesrv admin</title>
|
||||
<script type="module" crossorigin src="/assets/index-Q8RNNOYL.js"></script>
|
||||
<!doctype html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>telesrv admin</title>
|
||||
<script type="module" crossorigin src="/assets/index-BFkUM6v2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BaxMq_AT.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
|
||||
export type Language = "en" | "zh";
|
||||
export type Language = "en" | "zh" | "ru";
|
||||
export type TranslationParams = Record<string, string | number | boolean>;
|
||||
export type TFunction = (key: string, params?: TranslationParams) => string;
|
||||
|
||||
|
|
@ -83,6 +83,7 @@ const translations: Record<Language, Record<string, string>> = {
|
|||
"layout.logout": "Log out",
|
||||
"language.en": "EN",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "RU",
|
||||
"login.heading": "Operations Admin",
|
||||
"login.body": "Enter credentials to open the console.",
|
||||
"login.secret": "Admin password or token",
|
||||
|
|
@ -432,6 +433,7 @@ const translations: Record<Language, Record<string, string>> = {
|
|||
"layout.logout": "退出",
|
||||
"language.en": "EN",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "RU",
|
||||
"login.heading": "运维后台",
|
||||
"login.body": "输入凭据后进入控制台。",
|
||||
"login.secret": "管理员密码或 token",
|
||||
|
|
@ -704,6 +706,356 @@ const translations: Record<Language, Record<string, string>> = {
|
|||
"audit.dryRun": "预演",
|
||||
"audit.reason": "原因",
|
||||
"audit.time": "时间"
|
||||
},
|
||||
ru: {
|
||||
"app.adminConsole": "Панель администратора",
|
||||
"app.localAccess": "Локальный доступ",
|
||||
"app.title": "telesrv admin",
|
||||
"common.actions": "Действия",
|
||||
"common.admins": "Администраторы",
|
||||
"common.backToList": "Назад к списку",
|
||||
"common.channel": "Канал",
|
||||
"common.channelOrGroup": "Канал / Группа",
|
||||
"common.clear": "Очистить",
|
||||
"common.close": "Закрыть",
|
||||
"common.count": "Количество",
|
||||
"common.deleted": "Удалено",
|
||||
"common.detail": "Детали",
|
||||
"common.device": "Устройство",
|
||||
"common.disabled": "Отключено",
|
||||
"common.enabled": "Включено",
|
||||
"common.fromPeer": "От пира",
|
||||
"common.group": "Группа",
|
||||
"common.id": "ID",
|
||||
"common.limit": "Лимит",
|
||||
"common.loading": "Загрузка...",
|
||||
"common.member": "Участник",
|
||||
"common.members": "Участники",
|
||||
"common.messageId": "ID сообщения",
|
||||
"common.name": "Имя",
|
||||
"common.no": "Нет",
|
||||
"common.noResults": "Нет результатов",
|
||||
"common.none": "Нет",
|
||||
"common.normal": "Обычный",
|
||||
"common.operations": "Операции",
|
||||
"common.owner": "Владелец",
|
||||
"common.platform": "Платформа",
|
||||
"common.refresh": "Обновить",
|
||||
"common.search": "Поиск",
|
||||
"common.sender": "Отправитель",
|
||||
"common.status": "Статус",
|
||||
"common.survived": "Уцелело",
|
||||
"common.time": "Время",
|
||||
"common.type": "Тип",
|
||||
"common.updatedAt": "Обновлено",
|
||||
"common.username": "Имя пользователя",
|
||||
"common.valid": "Действителен",
|
||||
"common.verified": "Подтвержден",
|
||||
"common.views": "Просмотры",
|
||||
"common.yes": "Да",
|
||||
"route.accounts": "Аккаунты",
|
||||
"route.accountsSubtitle": "Консоль / Аккаунты",
|
||||
"route.channels": "Супергруппы и каналы",
|
||||
"route.channelsSubtitle": "Консоль / Каналы",
|
||||
"route.dashboard": "Панель управления",
|
||||
"route.dashboardSubtitle": "Консоль / Обзор",
|
||||
"route.messages": "Аудит сообщений",
|
||||
"route.messagesSubtitle": "Консоль / Сообщения",
|
||||
"route.gifts": "Звездные подарки",
|
||||
"route.giftsSubtitle": "Консоль / Звездные подарки",
|
||||
"layout.navigation": "Навигация",
|
||||
"layout.primaryNav": "Основное меню",
|
||||
"layout.dashboard": "Обзор",
|
||||
"layout.accounts": "Аккаунты",
|
||||
"layout.channels": "Супергруппы / Каналы",
|
||||
"layout.messages": "Сообщения",
|
||||
"layout.gifts": "Звездные подарки",
|
||||
"layout.privateMessages": "Личные",
|
||||
"layout.groupMessages": "Группы",
|
||||
"layout.runtime": "Среда выполнения",
|
||||
"layout.adminBackend": "Админ-бэкенд",
|
||||
"layout.ready": "Готов",
|
||||
"layout.pgRead": "Чтение из PG",
|
||||
"layout.readOnly": "Только чтение",
|
||||
"layout.writeOps": "Операции записи",
|
||||
"layout.dryRun": "Тестовый запуск",
|
||||
"layout.actor": "Вход выполнен как: {actor}",
|
||||
"layout.logout": "Выйти",
|
||||
"language.en": "EN",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "RU",
|
||||
"login.heading": "Панель администратора",
|
||||
"login.body": "Введите учетные данные для входа в консоль.",
|
||||
"login.secret": "Пароль или токен администратора",
|
||||
"login.submit": "Войти",
|
||||
"login.submitting": "Вход...",
|
||||
"dashboard.eyebrow": "Состояние системы",
|
||||
"dashboard.title": "Обзор консоли",
|
||||
"dashboard.readPath": "Путь чтения",
|
||||
"dashboard.readPathValue": "PG только для чтения",
|
||||
"dashboard.writePath": "Путь записи",
|
||||
"dashboard.executionPolicy": "Политика выполнения",
|
||||
"dashboard.dryRunFirst": "Сначала тестовый запуск",
|
||||
"dashboard.accountsText": "Статус аккаунтов, премиум, верификация, сессии.",
|
||||
"dashboard.channelsText": "Публичные каналы и группы, количество участников, статус верификации.",
|
||||
"dashboard.messagesText": "Ящики сообщений, обновления, состояние исходящих.",
|
||||
"dashboard.strip.dryRun": "Все опасные действия начинаются с тестового запуска",
|
||||
"dashboard.strip.token": "Браузер никогда не сохраняет внутренние токены",
|
||||
"dashboard.strip.pagination": "Списки используют курсорную пагинацию",
|
||||
"dashboard.strip.snapshot": "Детальные страницы сохраняют моментальные снимки исходного состояния",
|
||||
"account.pageTitle": "Аккаунты",
|
||||
"account.queryResults": "Результаты поиска",
|
||||
"account.recentActive": "Недавно активные аккаунты",
|
||||
"account.currentPage": "Аккаунты на странице",
|
||||
"account.onlineDevices": "Активные сессии устройств",
|
||||
"account.premium": "Premium",
|
||||
"account.frozen": "Заморожен",
|
||||
"account.searchPlaceholder": "ID пользователя / телефон / имя пользователя",
|
||||
"account.userID": "ID пользователя",
|
||||
"account.phone": "Телефон",
|
||||
"account.lastActive": "Последняя активность",
|
||||
"account.notVerified": "Не подтвержден",
|
||||
"account.notPremium": "Без Premium",
|
||||
"account.premiumUntil": "Premium истекает",
|
||||
"account.starsBalance": "Баланс Звезд",
|
||||
"account.startingGrantApplied": "стартовый бонус начислен",
|
||||
"account.startingGrantPending": "ожидает стартового бонуса",
|
||||
"account.activeSessions": "Авторизованные устройства",
|
||||
"account.accountFlags": "Флаги аккаунта",
|
||||
"account.restriction": "Ограничение",
|
||||
"account.restricted": "Ограничен",
|
||||
"account.createdAt": "Создан",
|
||||
"account.detailTitle": "Аккаунт #{id}",
|
||||
"account.profile": "Профиль аккаунта",
|
||||
"account.loadingDetail": "Загрузка данных аккаунта",
|
||||
"account.waitingData": "Ожидание данных",
|
||||
"account.noUsername": "Нет имени пользователя",
|
||||
"account.noPhone": "Нет телефона",
|
||||
"account.accountFrozen": "Аккаунт заморожен",
|
||||
"account.accountActive": "Аккаунт активен",
|
||||
"account.authorizationsTitle": "Авторизованные устройства",
|
||||
"account.authorizationsCount": "Авторизаций: {count}",
|
||||
"account.recentAdminOps": "Последние действия администратора",
|
||||
"account.recent30Audit": "Последние 30 записей аудита",
|
||||
"account.actionDock": "Действия с аккаунтом",
|
||||
"account.freezeAccount": "Заморозить аккаунт",
|
||||
"account.updateFreeze": "Обновить параметры заморозки",
|
||||
"account.unfreezeAccount": "Разморозить аккаунт",
|
||||
"account.freezeSince": "Заморожен с",
|
||||
"account.freezeUntil": "Срок подачи апелляции",
|
||||
"account.freezeUntilAria": "Срок подачи апелляции на заморозку",
|
||||
"account.freezeAppealURL": "URL для апелляции",
|
||||
"account.freezeAppealURLAria": "URL для апелляции на заморозку",
|
||||
"account.premiumMonths": "Срок действия Premium (в месяцах)",
|
||||
"account.premiumMonthsAria": "Указать срок действия Premium в месяцах",
|
||||
"account.setPremium": "Выдать Premium",
|
||||
"account.clearPremium": "Снять Premium",
|
||||
"account.starsAmount": "Количество звёзд",
|
||||
"account.starsAmountAria": "Указать количество начисляемых звёзд",
|
||||
"account.grantStars": "Начислить звёзды",
|
||||
"account.setVerified": "Подтвердить аккаунт",
|
||||
"account.clearVerified": "Снять подтверждение",
|
||||
"channel.pageTitle": "Супергруппы и каналы",
|
||||
"channel.recentUpdated": "Недавно обновленные",
|
||||
"channel.currentPage": "Объекты на странице",
|
||||
"channel.megagroups": "Супергруппы",
|
||||
"channel.broadcasts": "Каналы",
|
||||
"channel.verifiedCount": "Подтверждено",
|
||||
"channel.searchPlaceholder": "ID канала / имя пользователя / название",
|
||||
"channel.channelID": "ID канала",
|
||||
"channel.kind": "Тип",
|
||||
"channel.title": "Название",
|
||||
"channel.pts": "PTS",
|
||||
"channel.detailProfile": "Профиль канала",
|
||||
"channel.loadingDetail": "Загрузка данных канала",
|
||||
"channel.creator": "Создатель: {id}",
|
||||
"channel.governance": "Модерация",
|
||||
"channel.governanceValue": "Заблокировано {banned} / Исключено {kicked}",
|
||||
"channel.flags": "Флаги канала",
|
||||
"channel.rawRow": "Исходная строка БД",
|
||||
"channel.rawRowText": "Снимок базы данных только для чтения",
|
||||
"channel.actionDock": "Действия с каналом",
|
||||
"channel.setVerified": "Подтвердить канал",
|
||||
"channel.clearVerified": "Снять подтверждение",
|
||||
"channel.kind.broadcast": "Канал",
|
||||
"channel.kind.forum": "Супергруппа / Форум",
|
||||
"channel.kind.megagroup": "Супергруппа",
|
||||
"channel.kind.generic": "Канал / Группа",
|
||||
"messages.privateTitle": "Личные сообщения",
|
||||
"messages.privateEyebrow": "Личные ящики сообщений",
|
||||
"messages.groupTitle": "Групповые сообщения",
|
||||
"messages.groupEyebrow": "Сообщения супергрупп и каналов",
|
||||
"messages.selectPrivatePeers": "Сначала найдите и выберите владельца и собеседника",
|
||||
"messages.selectChannel": "Сначала найдите и выберите супергруппу или канал",
|
||||
"messages.ownerUser": "Пользователь-владелец",
|
||||
"messages.peerUser": "Собеседник",
|
||||
"messages.beforeDatePlaceholder": "курсор before_date",
|
||||
"messages.beforeIDPlaceholder": "курсор before_msg_id",
|
||||
"messages.limitPlaceholder": "лимит <= 100",
|
||||
"messages.searchMessages": "Поиск сообщений",
|
||||
"messages.nextPage": "Следующая страница",
|
||||
"messages.currentPage": "Сообщения на странице",
|
||||
"messages.deleted": "Удалено",
|
||||
"messages.outgoing": "Исходящее",
|
||||
"messages.incoming": "Входящее",
|
||||
"messages.ownerPeer": "Владелец / Собеседник",
|
||||
"messages.deleteSelected": "Указать и удалить выбранные сообщения",
|
||||
"messages.idsPlaceholder": "ID сообщений через запятую",
|
||||
"messages.revoke": "Удалить для обеих сторон",
|
||||
"messages.previewDelete": "Тестовое удаление",
|
||||
"messages.clearHistory": "Очистить историю личной переписки",
|
||||
"messages.maxIDPlaceholder": "граница max_id",
|
||||
"messages.maxBatchesPlaceholder": "max_batches",
|
||||
"messages.justClear": "Очистить только у себя",
|
||||
"messages.previewClearHistory": "Тестовая очистка истории",
|
||||
"messages.direction": "Направление",
|
||||
"messages.body": "Текст сообщения",
|
||||
"messages.privateDetailTitle": "Сообщение #{id}",
|
||||
"messages.detailEyebrow": "Детали сообщения",
|
||||
"messages.backPrivate": "Назад к личным сообщениям",
|
||||
"messages.backGroup": "Назад к групповым сообщениям",
|
||||
"messages.ownerPeerTitle": "Владелец {owner} · Собеседник {peer}",
|
||||
"messages.senderSubtitle": "Отправитель {sender} · {date}",
|
||||
"messages.boxID": "ID ящика сообщений",
|
||||
"messages.privateMessageID": "ID личного сообщения",
|
||||
"messages.messageSender": "Отправитель сообщения",
|
||||
"messages.messageBox": "Ящик сообщений",
|
||||
"messages.dialogRow": "Строка диалога",
|
||||
"messages.privateRow": "Строка личного сообщения",
|
||||
"messages.channelMessageRow": "Строка сообщения канала",
|
||||
"messages.channelRow": "Строка канала",
|
||||
"messages.userUpdateEvents": "События обновления пользователей",
|
||||
"messages.channelUpdateEvents": "События обновления каналов",
|
||||
"messages.eventJson": "JSON события",
|
||||
"messages.dispatchOutbox": "Очередь отправки (Outbox)",
|
||||
"messages.messageBoxesSnapshot": "Снимок message_boxes только для чтения",
|
||||
"messages.dialogSnapshot": "Снимок dialogs только для чтения",
|
||||
"messages.privateSnapshot": "Снимок private_messages только для чтения",
|
||||
"messages.channelMessagesSnapshot": "Снимок channel_messages только для чтения",
|
||||
"messages.channelSnapshot": "Снимок channels только для чтения",
|
||||
"messages.userEventsSource": "постоянные user_update_events",
|
||||
"messages.channelEventsSource": "постоянные channel_update_events",
|
||||
"messages.outboxSource": "онлайн/офлайн dispatch_outbox",
|
||||
"messages.attempts": "Попытки",
|
||||
"messages.deleteThis": "Удалить это сообщение",
|
||||
"messages.groupDetailTitle": "Групповое сообщение #{id}",
|
||||
"messages.channelGroupTitle": "Канал / Группа {id}",
|
||||
"messages.mediaCount": "С медиафайлами",
|
||||
"messages.channelPosts": "Посты канала",
|
||||
"messages.channelGroup": "Канал / Группа",
|
||||
"messages.pinned": "Закреплено",
|
||||
"messages.channelPost": "Пост в канале",
|
||||
"gifts.pageTitle": "Каталог звездных подарков",
|
||||
"gifts.eyebrow": "Каталог, неизменяемые версии и файлы анимаций",
|
||||
"gifts.total": "Подарков в каталоге",
|
||||
"gifts.enabled": "Включено",
|
||||
"gifts.received": "Полученные подарки",
|
||||
"gifts.formats": "Поддерживаемые форматы",
|
||||
"gifts.add": "Добавить подарок",
|
||||
"gifts.searchPlaceholder": "Поиск по ID подарка, названию или формату",
|
||||
"gifts.listSummary": "Показано {shown} из {total}",
|
||||
"gifts.idRevision": "ID / Версия",
|
||||
"gifts.price": "Цена / Конвертация",
|
||||
"gifts.importTitle": "Импорт звездного подарка",
|
||||
"gifts.importEyebrow": "Управление каталогом подарков",
|
||||
"gifts.newRevision": "Создать версию для подарка #{id}",
|
||||
"gifts.importHint": "Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.",
|
||||
"gifts.animation": "Файл анимации",
|
||||
"gifts.filePrompt": "Перетащите или выберите файл TGS / Lottie",
|
||||
"gifts.fileHint": "TGS, JSON или Lottie · файл проверяется перед импортом",
|
||||
"gifts.chooseFile": "Выбрать файл",
|
||||
"gifts.changeFile": "Изменить файл",
|
||||
"gifts.title": "Отображаемое название",
|
||||
"gifts.titlePlaceholder": "например, Праздничная звезда",
|
||||
"gifts.stars": "Цена в Звездах",
|
||||
"gifts.convertStars": "Звезд при конвертации",
|
||||
"gifts.sortOrder": "Порядок сортировки",
|
||||
"gifts.reason": "Причина для аудита",
|
||||
"gifts.reasonPlaceholder": "Кратко опишите причину импорта этого подарка",
|
||||
"gifts.enableAfterImport": "Включить после импорта",
|
||||
"gifts.validate": "Тестовая проверка",
|
||||
"gifts.confirmImport": "Подтвердить импорт",
|
||||
"gifts.stepDetails": "Файл и описание",
|
||||
"gifts.stepValidate": "Тестовая проверка",
|
||||
"gifts.stepImport": "Подтверждение импорта",
|
||||
"gifts.fileRequired": "Сначала выберите файл TGS или Lottie",
|
||||
"gifts.source": "Источник",
|
||||
"gifts.replace": "Новая версия",
|
||||
"gifts.disable": "Отключить",
|
||||
"gifts.enable": "Включить",
|
||||
"gifts.empty": "Звездные подарки еще не импортированы.",
|
||||
"gifts.emptyHint": "Импортируйте первую анимацию, чтобы начать наполнение каталога.",
|
||||
"gifts.validationReady": "Проверка пройдена",
|
||||
"gifts.validationHint": "Проверьте нормализованные метаданные и подтвердите импорт.",
|
||||
"gifts.confirmState": "Применить проверенные изменения состояния к подарку #{id}?",
|
||||
"collectibles.manage": "Пул атрибутов",
|
||||
"collectibles.title": "Пул коллекционных предметов · Подарок #{id}",
|
||||
"collectibles.eyebrow": "Уникальные атрибуты подарка",
|
||||
"collectibles.activeRevision": "Опубликованная версия {revision}",
|
||||
"collectibles.published": "Опубликовано",
|
||||
"collectibles.noPool": "Нет опубликованного пула коллекционных предметов",
|
||||
"collectibles.noPoolHint": "Опубликуйте модели, узоры и фоны для активации улучшений.",
|
||||
"collectibles.publishNew": "Опубликовать новую неизменяемую версию",
|
||||
"collectibles.immutableHint": "Тестовый запуск проверяет каждый файл и итоговые показатели редкости перед тем, как версия станет активной.",
|
||||
"collectibles.upgradeStars": "Цена улучшения в Звездах",
|
||||
"collectibles.supply": "Уникальный тираж",
|
||||
"collectibles.slug": "Публичный префикс ссылки (slug)",
|
||||
"collectibles.models": "Модели",
|
||||
"collectibles.patterns": "Узоры",
|
||||
"collectibles.backdrops": "Фоны",
|
||||
"collectibles.model": "Модель",
|
||||
"collectibles.pattern": "Узор",
|
||||
"collectibles.backdrop": "Фон",
|
||||
"collectibles.rarity": "Редкость ‰",
|
||||
"collectibles.rarityHint": "Сумма по каждому разделу должна составлять ровно 1000‰.",
|
||||
"collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения Telegram.",
|
||||
"collectibles.addAttribute": "Добавить",
|
||||
"collectibles.remove": "Удалить атрибут",
|
||||
"collectibles.fileRequired": "Для каждой модели и узора требуется файл TGS или Lottie.",
|
||||
"collectibles.backdropID": "ID фона",
|
||||
"collectibles.color.center": "Центр",
|
||||
"collectibles.color.edge": "Край",
|
||||
"collectibles.color.pattern": "Узор",
|
||||
"collectibles.color.text": "Текст",
|
||||
"collectibles.validationReady": "Пул атрибутов корректен",
|
||||
"collectibles.validationHint": "Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.",
|
||||
"collectibles.publish": "Опубликовать версию",
|
||||
"messages.msgIDsInvalid": "Некорректные ID сообщений",
|
||||
"auth.device": "Устройство",
|
||||
"auth.platform": "Платформа",
|
||||
"auth.ip": "IP-адрес",
|
||||
"auth.lastActive": "Последняя активность",
|
||||
"auth.revokeCurrent": "Отозвать текущую",
|
||||
"auth.keepCurrent": "Оставить текущую",
|
||||
"auth.revokeAll": "Разлогинить все устройства",
|
||||
"picker.userPlaceholder": "Поиск по user_id / телефону / имени пользователя",
|
||||
"picker.channelPlaceholder": "Поиск по channel_id / имени пользователя / названию",
|
||||
"picker.verified": "Подтвержденные",
|
||||
"picker.regular": "Обычные",
|
||||
"action.reasonRequired": "Пожалуйста, укажите причину операции",
|
||||
"action.flow": "Процесс выполнения",
|
||||
"action.close": "Закрыть",
|
||||
"action.stepReason": "Укажите причину",
|
||||
"action.stepDryRun": "Тестовый запуск",
|
||||
"action.stepConfirm": "Подтверждение выполнения",
|
||||
"action.reason": "Причина операции",
|
||||
"action.reasonPlaceholder": "Опишите, почему выполняется эта операция",
|
||||
"action.requestPreview": "Запросить предпросмотр",
|
||||
"action.result": "Результат действия",
|
||||
"action.commandID": "ID команды",
|
||||
"action.status": "Статус",
|
||||
"action.dryRun": "Тестовый запуск",
|
||||
"action.runAgain": "Запустить тестовый запуск снова",
|
||||
"action.runDry": "Сначала выполните тестовый запуск",
|
||||
"action.confirm": "Подтвердить выполнение",
|
||||
"audit.id": "ID",
|
||||
"audit.commandID": "ID команды",
|
||||
"audit.action": "Действие",
|
||||
"audit.actor": "Исполнитель",
|
||||
"audit.status": "Статус",
|
||||
"audit.dryRun": "Тестовый запуск",
|
||||
"audit.reason": "Причина",
|
||||
"audit.time": "Время"
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -724,7 +1076,8 @@ export function I18nProvider({ children }: { children: ReactNode }) {
|
|||
} catch {
|
||||
// Language persistence is best-effort.
|
||||
}
|
||||
document.documentElement.lang = lang === "zh" ? "zh-CN" : "en";
|
||||
const langAttr = lang === "zh" ? "zh-CN" : lang === "ru" ? "ru" : "en";
|
||||
document.documentElement.lang = langAttr;
|
||||
document.documentElement.dir = "ltr";
|
||||
document.documentElement.setAttribute("translate", "no");
|
||||
document.body.classList.add("notranslate");
|
||||
|
|
@ -752,7 +1105,7 @@ export function LanguageSwitch() {
|
|||
const { lang, setLang, t } = useI18n();
|
||||
return (
|
||||
<div className="language-switch" role="group" aria-label="Language">
|
||||
{(["en", "zh"] as const).map((item) => (
|
||||
{(["en", "zh", "ru"] as const).map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
className={lang === item ? "active" : ""}
|
||||
|
|
@ -813,5 +1166,8 @@ function normalizeLanguage(value: string | null | undefined): Language | null {
|
|||
if (tag === "en" || tag.startsWith("en-")) {
|
||||
return "en";
|
||||
}
|
||||
if (tag === "ru" || tag.startsWith("ru-")) {
|
||||
return "ru";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue