config: add env schema and localization foundation
This commit is contained in:
+63
-3
@@ -31,25 +31,85 @@ ONLY_FOR_CREATOR_MODE=false
|
||||
# Use user names in AI prompts
|
||||
USE_NAMES_IN_PROMPT=true
|
||||
|
||||
# Custom system prompt for AI (or put it into data/SYSTEM_PROMPT.md)
|
||||
SYSTEM_PROMPT=
|
||||
|
||||
# Maximum photo size in pixels
|
||||
MAX_PHOTO_SIZE=1280
|
||||
|
||||
# Directory with localization JSON files
|
||||
LOCALES_DIR=locales
|
||||
|
||||
# ============================================
|
||||
# AI MODELS CONFIGURATION (Optional)
|
||||
# ============================================
|
||||
|
||||
# Google Gemini
|
||||
GEMINI_API_KEY=
|
||||
# google: official Gemini API via @google/genai; openai: OpenAI-compatible Gemini endpoint; auto: infer from GEMINI_BASE_URL
|
||||
GEMINI_API_MODE=google
|
||||
GEMINI_MODEL=gemini-2.5-flash
|
||||
GEMINI_IMAGE_MODEL=gemini-2.5-flash-image
|
||||
GEMINI_TRANSCRIPTION_MODEL=gemini-2.5-flash
|
||||
GEMINI_TTS_MODEL=gemini-2.5-flash-preview-tts
|
||||
GEMINI_TTS_VOICE=Kore
|
||||
GEMINI_MAX_CONCURRENT_REQUESTS=3
|
||||
|
||||
# Mistral AI
|
||||
MISTRAL_API_KEY=
|
||||
MISTRAL_MODEL=mistral-small-latest
|
||||
MISTRAL_TRANSCRIPTION_MODEL=voxtral-mini-latest
|
||||
MISTRAL_TTS_MODEL=
|
||||
MISTRAL_TTS_VOICE_ID=
|
||||
MISTRAL_MAX_CONCURRENT_REQUESTS=3
|
||||
|
||||
# Ollama (Local AI Model)
|
||||
OLLAMA_ADDRESS=
|
||||
OLLAMA_MODEL=
|
||||
OLLAMA_CHAT_MODEL=
|
||||
OLLAMA_IMAGE_MODEL=
|
||||
OLLAMA_THINK_MODEL=
|
||||
OLLAMA_AUDIO_MODEL=gemma4:e2b
|
||||
OLLAMA_API_KEY=
|
||||
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
|
||||
OLLAMA_RAG_CHUNK_SIZE=1400
|
||||
OLLAMA_RAG_CHUNK_OVERLAP=220
|
||||
OLLAMA_RAG_TOP_K=8
|
||||
OLLAMA_RAG_MAX_CONTEXT_CHARS=14000
|
||||
OLLAMA_RAG_MIN_SCORE=0.12
|
||||
OLLAMA_RAG_MAX_ARCHIVE_FILES=200
|
||||
OLLAMA_RAG_MAX_ARCHIVE_BYTES=52428800
|
||||
OLLAMA_RAG_MAX_ARCHIVE_DEPTH=2
|
||||
OLLAMA_MAX_CONCURRENT_REQUESTS=1
|
||||
|
||||
# Custom system prompt for AI
|
||||
SYSTEM_PROMPT=
|
||||
# OpenAI
|
||||
OPENAI_API_KEY=
|
||||
OPENAI_BASE_URL=
|
||||
OPENAI_MODEL=gpt-4.1-nano
|
||||
OPENAI_IMAGE_MODEL=gpt-image-1-mini
|
||||
OPENAI_TRANSCRIPTION_MODEL=gpt-4o-mini-transcribe
|
||||
OPENAI_TTS_MODEL=gpt-4o-mini-tts
|
||||
OPENAI_TTS_VOICE=alloy
|
||||
OPENAI_TTS_INSTRUCTIONS=
|
||||
OPENAI_MAX_CONCURRENT_REQUESTS=3
|
||||
|
||||
# Per-capability AI endpoint overrides
|
||||
# Pattern:
|
||||
# <PROVIDER>_<CAPABILITY>_MODEL=
|
||||
# <PROVIDER>_<CAPABILITY>_BASE_URL=
|
||||
# <PROVIDER>_<CAPABILITY>_API_KEY=
|
||||
#
|
||||
# Providers: OLLAMA, GEMINI, MISTRAL, OPENAI
|
||||
# Capabilities: CHAT, VISION, OCR, THINKING, EXTENDED_THINKING, TOOLS, AUDIO,
|
||||
# DOCUMENTS, OUTPUT_IMAGES, SPEECH_TO_TEXT, TEXT_TO_SPEECH
|
||||
# Endpoint aliases are also supported: *_ENDPOINT and *_ADDRESS.
|
||||
# Provider-wide fallback endpoints: OPENAI_BASE_URL, MISTRAL_BASE_URL,
|
||||
# GEMINI_BASE_URL, OLLAMA_ADDRESS or OLLAMA_BASE_URL.
|
||||
# Capability aliases are also supported: IMAGE, THINK, RAG, EMBEDDING,
|
||||
# TRANSCRIPTION, STT, TTS.
|
||||
#
|
||||
# Examples:
|
||||
# OPENAI_SPEECH_TO_TEXT_MODEL=gpt-4o-mini-transcribe
|
||||
# OPENAI_SPEECH_TO_TEXT_BASE_URL=https://api.openai.com/v1
|
||||
# OPENAI_SPEECH_TO_TEXT_API_KEY=
|
||||
# MISTRAL_TTS_BASE_URL=
|
||||
# OLLAMA_DOCUMENTS_ADDRESS=http://localhost:11434
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"language": {
|
||||
"default": "Default",
|
||||
"en": "English",
|
||||
"ru": "Russian",
|
||||
"ua": "Ukrainian",
|
||||
"instructionName": "English"
|
||||
},
|
||||
"providerChoice.default": "Default",
|
||||
"errorText": "⚠️ An error occurred.",
|
||||
"waitThinkText": "⏳ Let me think...",
|
||||
"analyzingPictureText": "🔍 Analyzing the image...",
|
||||
"analyzingPicturesText": "🔍 Analyzing the images...",
|
||||
"reasoningText": "🤔 Reasoning...",
|
||||
"transcribingAudioText": "🦻 Transcribing audio...",
|
||||
"genImageText": "👨🎨 Generating an image...",
|
||||
"cancelText": "❌ Cancel",
|
||||
"regenerateText": "🔄 Regenerate",
|
||||
"aiCancelCallbackText": "Cancel AI generation",
|
||||
"aiRegenerateCallbackText": "Regenerate AI response",
|
||||
"userSettingsCallbackText": "User settings",
|
||||
"noAccessText": "No access",
|
||||
"notBotCreatorText": "You are not the bot creator.",
|
||||
"notBotAdministratorText": "You are not a bot administrator.",
|
||||
"notAChatText": "This is not a chat.",
|
||||
"notChatAdministratorText": "You are not a chat administrator.",
|
||||
"botNotChatAdministratorText": "The bot is not a chat administrator.",
|
||||
"replyRequiredText": "A reply to a message is required.",
|
||||
"onlyOriginalAuthorText": "Only the author of the original message can perform this action.",
|
||||
"commandsHeaderText": "Commands:\n\n",
|
||||
"sentCommandsInDmText": "Sent commands in DM 😎",
|
||||
"couldNotSendCommandsInDmText": "Could not send commands in DM ☹️\nSending them here instead",
|
||||
"administratorsHeaderText": "*Administrators*:\n\n",
|
||||
"noUserInfoText": "No user information",
|
||||
"useLeaveCommandText": "Use /leave",
|
||||
"databaseBackupCaption": "Database backup",
|
||||
"databaseBackupSentText": "Successfully sent to the creator in DM!",
|
||||
"noChoicesText": "Nothing to choose from",
|
||||
"qrCodeMissingTextText": "No text found for QR code generation.",
|
||||
"quoteMissingTextText": "Could not find text in the message 😢",
|
||||
"quoteBuildFailedText": "Could not build the quote 😢",
|
||||
"speechToTextInstructionText": "Send audio/voice/video-note or reply with /stt to a message containing audio.",
|
||||
"speechToTextEmptyResultText": "Speech-to-text did not return transcription text.",
|
||||
"textToSpeechInstructionText": "Send text after the command or reply with /tts to a message containing text.",
|
||||
"titleMissingText": "Could not find a title...",
|
||||
"betterFallbackText": "Better",
|
||||
"pongText": "pong",
|
||||
"modelListHeaderText": "Available models:\n\n",
|
||||
"modelListLoadFailedText": "Could not load the model list",
|
||||
"noCurrentModelText": "Model is not set. Use one of the listed values.",
|
||||
"unsupportedAttachmentText": "This attachment type is not supported.",
|
||||
"attachmentMissingFromCacheText": "Attachment file is missing from cache.",
|
||||
"noSupportedTranscriptionProviderText": "No supported speech-to-text provider is configured.",
|
||||
"noSupportedTextToSpeechProviderText": "No supported text-to-speech provider is configured.",
|
||||
"noSpeechToTextProviderForAccessText": "No speech-to-text providers are configured for your access level.",
|
||||
"noTextToSpeechProviderForAccessText": "No text-to-speech providers are configured for your access level.",
|
||||
"noTextToSynthesizeText": "No text to synthesize.",
|
||||
"speechFileTooLargeText": "The speech file is larger than 50 MB and cannot be sent.",
|
||||
"userSettingsTitle": "User Settings",
|
||||
"userSettingsAiProviderSelectionTitle": "AI Provider Selection",
|
||||
"userSettingsInterfaceLanguageSelectionTitle": "Interface Language Selection",
|
||||
"userSettingsResponseLanguageSelectionTitle": "Response Language Selection",
|
||||
"userSettingsContextSizeSelectionTitle": "Context Size Selection",
|
||||
"userSettingsVoiceModeSelectionTitle": "Voice Message Mode Selection",
|
||||
"userSettingsTierLabel": "Tier",
|
||||
"userSettingsAiProviderLabel": "AI provider",
|
||||
"userSettingsInterfaceLanguageLabel": "Interface language",
|
||||
"userSettingsResponseLanguageLabel": "LLM response language",
|
||||
"userSettingsContextSizeLabel": "Context size",
|
||||
"userSettingsVoiceModeLabel": "Voice messages",
|
||||
"userSettingsBackButtonText": "Back",
|
||||
"userSettingsAiProviderButtonPrefix": "AI provider",
|
||||
"userSettingsInterfaceLanguageButtonPrefix": "Interface language",
|
||||
"userSettingsResponseLanguageButtonPrefix": "Response language",
|
||||
"userSettingsContextSizeButtonPrefix": "Context",
|
||||
"userSettingsVoiceModeButtonPrefix": "Voice",
|
||||
"userSettingsCreatorTierText": "Creator",
|
||||
"userSettingsAdminTierText": "Admin",
|
||||
"userSettingsUserTierText": "User",
|
||||
"userSettingsSelectedPrefix": "✓ ",
|
||||
"userSettingsContextSizeDefaultText": "Default",
|
||||
"userSettingsVoiceModeExecuteText": "Run through AI",
|
||||
"userSettingsVoiceModeTranscriptText": "Show transcript only",
|
||||
"startingImageGenText": "🌈 Starting image generation...",
|
||||
"imageGenText": "🌈 Generating image...",
|
||||
"finalizingImageGenText": "🌈 Finalizing image generation...",
|
||||
"botCannotMakeItselfAdminText": "The bot cannot make itself an admin",
|
||||
"botCreatorAlreadyAdminText": "The bot creator is already an admin",
|
||||
"botCannotRemoveItselfFromAdminsText": "The bot cannot remove itself from admins",
|
||||
"botCreatorCannotStopBeingAdminText": "The bot creator cannot stop being an admin",
|
||||
"botWillNotBanCreatorText": "The bot will not ban its creator.",
|
||||
"botWillNotBanAdminsText": "The bot will not ban its administrators.",
|
||||
"botIsNotBannedByItselfText": "The bot is not banned by itself anyway.",
|
||||
"botCreatorNeverBannedText": "The bot creator is not banned and never will be.",
|
||||
"botAdminsNotBannedText": "Bot administrators are not banned anyway.",
|
||||
"botWillNotIgnoreItselfText": "The bot will not ignore itself.",
|
||||
"botWillNotIgnoreCreatorText": "The bot will not ignore its creator.",
|
||||
"botWillNotIgnoreAdminsText": "The bot will not ignore its administrators.",
|
||||
"botIsNotIgnoredByItselfText": "The bot is not ignored by itself anyway.",
|
||||
"botCreatorNotIgnoredText": "The bot creator is not ignored and never will be.",
|
||||
"botAdminsNotIgnoredText": "Bot administrators are not ignored anyway.",
|
||||
"botAlreadyAlwaysListensToItselfText": "The bot already always listens to itself",
|
||||
"botAlwaysListensToCreatorText": "The bot always listens to its creator",
|
||||
"coinHeadsText": "Heads",
|
||||
"coinTailsText": "Tails",
|
||||
"distortReplyInstructionText": "Reply with /distort to a message containing an image (photo, document, or sticker).\nExample: /distort 16 80",
|
||||
"distortMissingImageText": "I do not see an image in the reply. Send a photo or image file.",
|
||||
"shutdownFallbackText": "...",
|
||||
"shutdownSequenceTexts": [
|
||||
"well then, everyone",
|
||||
"it was nice talking to you",
|
||||
"but it is time for me to rest",
|
||||
"all the best"
|
||||
],
|
||||
"shutdownDoneText": "*R.I.P*",
|
||||
"whenNowText": "right now",
|
||||
"whenNeverText": "never",
|
||||
"whenYearUnitText": "year",
|
||||
"whenDayUnitText": "day",
|
||||
"whenWeekUnitText": "week",
|
||||
"whenMonthUnitText": "month",
|
||||
"whenHourUnitText": "hour",
|
||||
"whenMinuteUnitText": "minute",
|
||||
"whenSecondUnitText": "second",
|
||||
"getCancelledText": "{provider}\n❌ Generation cancelled.",
|
||||
"getPartialImageGenText": "🌈 Generating image ({iteration}/{total})...",
|
||||
"getImageGenDoneText.withModel": "👨🎨 Image generated. Model: `{model}`.",
|
||||
"getImageGenDoneText.default": "👨🎨 Image generated.",
|
||||
"getErrorText.withReason": "{errorText} Reason:\n{reason}",
|
||||
"getUseToolText.python": "👨💻 Running `Python`",
|
||||
"getUseToolText.default": "🔧 Using tool `{name}`",
|
||||
"getAnalyzingDocumentText.default": "🔍 Analyzing the document...",
|
||||
"getAnalyzingDocumentText.single": "🔍 Analyzing document: `{name}`",
|
||||
"getAnalyzingDocumentText.many": "🔍 Analyzing documents: {names}",
|
||||
"getPreparingRAGText.default": "🔍 Preparing RAG for the document...",
|
||||
"getPreparingRAGText.single": "🔍 Preparing RAG for document: `{name}`",
|
||||
"getPreparingRAGText.many": "🔍 Preparing RAG for documents: {names}",
|
||||
"getBuildingRAGIndexText.default": "🧠 Building RAG index...",
|
||||
"getBuildingRAGIndexText.withModel": "🧠 Building RAG index: `{modelName}`.",
|
||||
"queueNoneText": "none",
|
||||
"getAiQueueText.queued": "⏳ Request to {provider} is queued.",
|
||||
"getAiQueueText.ahead": "Requests ahead: {count}.",
|
||||
"getTelegramFileTooLargeText": "File {fileName} is larger than {maxSizeMb} MB and cannot be sent.",
|
||||
"getUserIsNowAdminText": "{name} is now an admin!",
|
||||
"getUserAlreadyAdminText": "{name} is already an admin 🤔",
|
||||
"getUserNoLongerAdminText": "{name} is no longer an admin!",
|
||||
"getUserWasNotAdminText": "{name} was not an admin 🤔",
|
||||
"getUserBannedText": "{name} banned 🚫",
|
||||
"getUserBanFailedText": "Could not ban {name} ☹️",
|
||||
"getUserUnbannedText": "{name} unbanned ⛓️💥",
|
||||
"getUserUnbanFailedText": "Could not unban {name} ☹️",
|
||||
"getUserIgnoredText": "{name} is muted! 🔇",
|
||||
"getUserAlreadyIgnoredText": "{name} is already muted 🤔",
|
||||
"getUserIgnoreFailedText": "Could not mute {name} ☹️",
|
||||
"getUserUnignoredText": "{name} is no longer muted! 🔈",
|
||||
"getUserWasNotIgnoredText": "{name} was not muted 🤔",
|
||||
"getUserUnignoreFailedText": "Could not unmute {name} ☹️",
|
||||
"getChoiceText": "Chose *{choice}*",
|
||||
"getCoinResultText": "It landed on *{result}*",
|
||||
"getLoadedModelsText": "Loaded models: {models}",
|
||||
"getSelectedModelText": "Selected model: `{model}`",
|
||||
"getCurrentModelText": "Current model: `{model}`",
|
||||
"getLoadingModelText": "Loading model `{model}`...",
|
||||
"getUserSettingsContextSizeText": "{size} tokens",
|
||||
"getQrCodeTextTooLongText": "Text is too long for QR ({actualLength} characters). It will be trimmed to {maxLength} characters.",
|
||||
"getQrCodeReadyText": "QR code ready ✅\nContent:\n<blockquote expandable>{content}</blockquote>",
|
||||
"getQrCodeFailedText": "Could not generate QR: {reason}",
|
||||
"getWhenPrefixText": "in ",
|
||||
"getWhenPluralUnitText": "{unit}s",
|
||||
"getWhenDurationText": "{prefix}{value} {unit}",
|
||||
"commandDescriptions": {
|
||||
"ae": "evaluation",
|
||||
"adminsAdd": "Add user to admins",
|
||||
"adminsRemove": "Remove user from admins",
|
||||
"ban": "ban user from chat",
|
||||
"choice": "Choose a random value",
|
||||
"coin": "Heads or tails",
|
||||
"debug": "Returns msg (or reply) as json",
|
||||
"dice": "Sends random or specific dice",
|
||||
"distort": "Distortion of picture",
|
||||
"geminiChat": "Chat with AI (Gemini)",
|
||||
"geminiGetModel": "Get current Gemini model",
|
||||
"geminiListModels": "List all Gemini models",
|
||||
"geminiSetModel": "Set Gemini model",
|
||||
"help": "Show list of commands",
|
||||
"id": "ID of chat, user and reply (if replied to any message)",
|
||||
"ignore": "Bot will ignore user",
|
||||
"info": "Info about bot",
|
||||
"leave": "Bot will leave current chat",
|
||||
"mistralChat": "Chat with AI (Mistral)",
|
||||
"mistralGetModel": "Get current Mistral model",
|
||||
"mistralListModels": "List all Mistral models",
|
||||
"mistralSetModel": "Set Mistral model",
|
||||
"ollamaChat": "Chat with AI (Ollama)",
|
||||
"ollamaGetModel": "Get current Ollama model",
|
||||
"ollamaListModels": "List all Ollama models",
|
||||
"ollamaSearch": "Web search via Ollama",
|
||||
"ollamaSetModel": "Set Ollama model",
|
||||
"openAiChat": "Chat with AI (OpenAI)",
|
||||
"openAiGetModel": "Get current OpenAI model",
|
||||
"openAiListModels": "List all OpenAI models",
|
||||
"openAiSetModel": "Set OpenAI model",
|
||||
"ping": "Ping between received and sent message",
|
||||
"qr": "Generates QR-code from text you sent or replied to.",
|
||||
"quote": "Make quote from text (or quote)",
|
||||
"randomInt": "Ranged random integer from parameters",
|
||||
"randomString": "literally random string (up to 4096 symbols)",
|
||||
"settings": "User settings",
|
||||
"shutdown": "Self-destruction sequence for bot (shutdown)",
|
||||
"speechToText": "Transcribe speech to text",
|
||||
"start": "Start the bot",
|
||||
"systemInfo": "System information",
|
||||
"textToSpeech": "Generate speech from text",
|
||||
"title": "Change group title",
|
||||
"test": "System functionality check",
|
||||
"transliteration": "Transliteration EN <--> RU",
|
||||
"unban": "unban user from chat",
|
||||
"unignore": "Bot will start responding to the user",
|
||||
"uptime": "Bot's uptime",
|
||||
"whatBetter": "either a or b randomly (50% chance)",
|
||||
"when": "random date"
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
{
|
||||
"language": {
|
||||
"default": "По умолчанию",
|
||||
"en": "Английский",
|
||||
"ru": "Русский",
|
||||
"ua": "Украинский",
|
||||
"instructionName": "Russian"
|
||||
},
|
||||
"providerChoice.default": "По умолчанию",
|
||||
"errorText": "⚠️ Произошла ошибка.",
|
||||
"waitThinkText": "⏳ Думаю...",
|
||||
"analyzingPictureText": "🔍 Анализирую изображение...",
|
||||
"analyzingPicturesText": "🔍 Анализирую изображения...",
|
||||
"reasoningText": "🤔 Рассуждаю...",
|
||||
"transcribingAudioText": "🦻 Распознаю аудио...",
|
||||
"genImageText": "👨🎨 Генерирую изображение...",
|
||||
"cancelText": "❌ Отмена",
|
||||
"regenerateText": "🔄 Сгенерировать заново",
|
||||
"aiCancelCallbackText": "Отменить генерацию ИИ",
|
||||
"aiRegenerateCallbackText": "Сгенерировать ответ ИИ заново",
|
||||
"userSettingsCallbackText": "Настройки пользователя",
|
||||
"noAccessText": "Нет доступа",
|
||||
"notBotCreatorText": "Вы не создатель бота.",
|
||||
"notBotAdministratorText": "Вы не администратор бота.",
|
||||
"notAChatText": "Это не чат.",
|
||||
"notChatAdministratorText": "Вы не администратор чата.",
|
||||
"botNotChatAdministratorText": "Бот не является администратором чата.",
|
||||
"replyRequiredText": "Нужно ответить на сообщение.",
|
||||
"onlyOriginalAuthorText": "Это действие доступно только автору исходного сообщения.",
|
||||
"dockerContainerLabelText": "Docker-контейнер",
|
||||
"processLabelText": "Процесс",
|
||||
"systemLabelText": "Система",
|
||||
"systemInfoOsLabelText": "ОС",
|
||||
"systemInfoRuntimeLabelText": "RUNTIME",
|
||||
"systemInfoDockerLabelText": "DOCKER",
|
||||
"systemInfoCpuLabelText": "CPU",
|
||||
"systemInfoRamLabelText": "RAM",
|
||||
"systemInfoCpuCoresText": "ядер",
|
||||
"systemInfoCpuThreadsText": "потоков",
|
||||
"idChatLabelText": "id чата",
|
||||
"idFromLabelText": "id пользователя",
|
||||
"idReplyLabelText": "id ответа",
|
||||
"runtimeProviderLabelText": "провайдер",
|
||||
"runtimeModelLabelText": "модель",
|
||||
"runtimeCapabilitiesLabelText": "возможности",
|
||||
"runtimeExternalLabelText": "внешний",
|
||||
"infoAiBlockLabelText": "AI",
|
||||
"infoSupportedProvidersLabelText": "поддерживаемые провайдеры",
|
||||
"infoToolsBlockLabelText": "инструменты",
|
||||
"infoCountLabelText": "количество",
|
||||
"infoCommandsBlockLabelText": "команды",
|
||||
"infoPublicLabelText": "публичные",
|
||||
"infoPrivateLabelText": "приватные",
|
||||
"infoChatLabelText": "чат",
|
||||
"infoCallbackLabelText": "колбэки",
|
||||
"commandsHeaderText": "Команды:\n\n",
|
||||
"sentCommandsInDmText": "Отправил команды в личные сообщения 😎",
|
||||
"couldNotSendCommandsInDmText": "Не получилось отправить команды в личные сообщения ☹️\nОтправляю их сюда",
|
||||
"administratorsHeaderText": "*Администраторы*:\n\n",
|
||||
"noUserInfoText": "Нет информации о пользователе",
|
||||
"useLeaveCommandText": "Используйте /leave",
|
||||
"databaseBackupCaption": "Резервная копия базы данных",
|
||||
"databaseBackupSentText": "Успешно отправил создателю в личные сообщения!",
|
||||
"noChoicesText": "Не из чего выбирать",
|
||||
"qrCodeMissingTextText": "Не найден текст для генерации QR-кода.",
|
||||
"quoteMissingTextText": "Не удалось найти текст в сообщении 😢",
|
||||
"quoteBuildFailedText": "Не удалось собрать цитату 😢",
|
||||
"speechToTextInstructionText": "Отправьте аудио/voice/video-note или ответьте /stt на сообщение с аудио.",
|
||||
"speechToTextEmptyResultText": "Распознавание речи не вернуло текст.",
|
||||
"textToSpeechInstructionText": "Отправьте текст после команды или ответьте /tts на сообщение с текстом.",
|
||||
"titleMissingText": "Не удалось найти заголовок...",
|
||||
"betterFallbackText": "Лучше",
|
||||
"pongText": "понг",
|
||||
"modelListHeaderText": "Доступные модели:\n\n",
|
||||
"modelListLoadFailedText": "Не удалось загрузить список моделей",
|
||||
"noCurrentModelText": "Модель не задана. Используйте одно из значений из списка.",
|
||||
"unsupportedAttachmentText": "Этот тип вложения не поддерживается.",
|
||||
"attachmentMissingFromCacheText": "Файл вложения отсутствует в кэше.",
|
||||
"noSupportedTranscriptionProviderText": "Не настроен ни один провайдер распознавания речи.",
|
||||
"noSupportedTextToSpeechProviderText": "Не настроен ни один провайдер синтеза речи.",
|
||||
"noSpeechToTextProviderForAccessText": "Для вашего уровня доступа не настроены провайдеры распознавания речи.",
|
||||
"noTextToSpeechProviderForAccessText": "Для вашего уровня доступа не настроены провайдеры синтеза речи.",
|
||||
"noTextToSynthesizeText": "Нет текста для синтеза речи.",
|
||||
"speechFileTooLargeText": "Файл речи больше 50 МБ и не может быть отправлен.",
|
||||
"userSettingsTitle": "Настройки пользователя",
|
||||
"userSettingsAiProviderSelectionTitle": "Выбор AI-провайдера",
|
||||
"userSettingsInterfaceLanguageSelectionTitle": "Выбор языка интерфейса",
|
||||
"userSettingsResponseLanguageSelectionTitle": "Выбор языка ответов",
|
||||
"userSettingsContextSizeSelectionTitle": "Выбор размера контекста",
|
||||
"userSettingsVoiceModeSelectionTitle": "Режим голосовых сообщений",
|
||||
"userSettingsTierLabel": "Уровень",
|
||||
"userSettingsAiProviderLabel": "AI-провайдер",
|
||||
"userSettingsInterfaceLanguageLabel": "Язык интерфейса",
|
||||
"userSettingsResponseLanguageLabel": "Язык ответов LLM",
|
||||
"userSettingsContextSizeLabel": "Размер контекста",
|
||||
"userSettingsVoiceModeLabel": "Голосовые сообщения",
|
||||
"userSettingsBackButtonText": "Назад",
|
||||
"userSettingsAiProviderButtonPrefix": "AI-провайдер",
|
||||
"userSettingsInterfaceLanguageButtonPrefix": "Язык интерфейса",
|
||||
"userSettingsResponseLanguageButtonPrefix": "Язык ответов",
|
||||
"userSettingsContextSizeButtonPrefix": "Контекст",
|
||||
"userSettingsVoiceModeButtonPrefix": "Голосовые",
|
||||
"userSettingsCreatorTierText": "Создатель",
|
||||
"userSettingsAdminTierText": "Админ",
|
||||
"userSettingsUserTierText": "Пользователь",
|
||||
"userSettingsSelectedPrefix": "✓ ",
|
||||
"userSettingsContextSizeDefaultText": "По умолчанию",
|
||||
"userSettingsVoiceModeExecuteText": "Выполнять через ИИ",
|
||||
"userSettingsVoiceModeTranscriptText": "Только расшифровка",
|
||||
"startingImageGenText": "🌈 Запускаю генерацию изображения...",
|
||||
"imageGenText": "🌈 Генерирую изображение...",
|
||||
"finalizingImageGenText": "🌈 Завершаю генерацию изображения...",
|
||||
"botCannotMakeItselfAdminText": "Бот не может назначить себя администратором",
|
||||
"botCreatorAlreadyAdminText": "Создатель бота уже администратор",
|
||||
"botCannotRemoveItselfFromAdminsText": "Бот не может удалить себя из администраторов",
|
||||
"botCreatorCannotStopBeingAdminText": "Создатель бота не может перестать быть администратором",
|
||||
"botWillNotBanCreatorText": "Бот не будет банить своего создателя.",
|
||||
"botWillNotBanAdminsText": "Бот не будет банить своих администраторов.",
|
||||
"botIsNotBannedByItselfText": "Бот и так не забанен сам собой.",
|
||||
"botCreatorNeverBannedText": "Создатель бота не забанен и никогда не будет.",
|
||||
"botAdminsNotBannedText": "Администраторы бота и так не забанены.",
|
||||
"botWillNotIgnoreItselfText": "Бот не будет игнорировать себя.",
|
||||
"botWillNotIgnoreCreatorText": "Бот не будет игнорировать своего создателя.",
|
||||
"botWillNotIgnoreAdminsText": "Бот не будет игнорировать своих администраторов.",
|
||||
"botIsNotIgnoredByItselfText": "Бот и так не игнорирует сам себя.",
|
||||
"botCreatorNotIgnoredText": "Создатель бота не игнорируется и никогда не будет.",
|
||||
"botAdminsNotIgnoredText": "Администраторы бота и так не игнорируются.",
|
||||
"botAlreadyAlwaysListensToItselfText": "Бот и так всегда слушает сам себя",
|
||||
"botAlwaysListensToCreatorText": "Бот всегда слушает своего создателя",
|
||||
"coinHeadsText": "Орёл",
|
||||
"coinTailsText": "Решка",
|
||||
"distortReplyInstructionText": "Ответьте /distort на сообщение с изображением (фото, документ или стикер).\nПример: /distort 16 80",
|
||||
"distortMissingImageText": "Не вижу изображения в ответе. Отправьте фото или файл изображения.",
|
||||
"shutdownFallbackText": "...",
|
||||
"shutdownSequenceTexts": [
|
||||
"ну что ж, народ",
|
||||
"было приятно пообщаться",
|
||||
"но мне пора отдохнуть",
|
||||
"всем добра"
|
||||
],
|
||||
"shutdownDoneText": "*R.I.P*",
|
||||
"whenNowText": "прямо сейчас",
|
||||
"whenNeverText": "никогда",
|
||||
"whenYearUnitText": "год",
|
||||
"whenDayUnitText": "день",
|
||||
"whenWeekUnitText": "неделя",
|
||||
"whenMonthUnitText": "месяц",
|
||||
"whenHourUnitText": "час",
|
||||
"whenMinuteUnitText": "минута",
|
||||
"whenSecondUnitText": "секунда",
|
||||
"getCancelledText": "{provider}\n❌ Генерация отменена.",
|
||||
"getPartialImageGenText": "🌈 Генерирую изображение ({iteration}/{total})...",
|
||||
"getImageGenDoneText.withModel": "👨🎨 Изображение сгенерировано. Модель: `{model}`.",
|
||||
"getImageGenDoneText.default": "👨🎨 Изображение сгенерировано.",
|
||||
"getErrorText.withReason": "{errorText} Причина:\n{reason}",
|
||||
"getUseToolText.python": "👨💻 Запускаю `Python`",
|
||||
"getUseToolText.default": "🔧 Использую инструмент `{name}`",
|
||||
"getAnalyzingDocumentText.default": "🔍 Анализирую документ...",
|
||||
"getAnalyzingDocumentText.single": "🔍 Анализирую документ: `{name}`",
|
||||
"getAnalyzingDocumentText.many": "🔍 Анализирую документы: {names}",
|
||||
"getPreparingRAGText.default": "🔍 Готовлю RAG для документа...",
|
||||
"getPreparingRAGText.single": "🔍 Готовлю RAG для документа: `{name}`",
|
||||
"getPreparingRAGText.many": "🔍 Готовлю RAG для документов: {names}",
|
||||
"getBuildingRAGIndexText.default": "🧠 Строю RAG-индекс...",
|
||||
"getBuildingRAGIndexText.withModel": "🧠 Строю RAG-индекс: `{modelName}`.",
|
||||
"queueNoneText": "нет",
|
||||
"getAiQueueText.queued": "⏳ Запрос к {provider} поставлен в очередь.",
|
||||
"getAiQueueText.ahead": "Запросов впереди: {count}.",
|
||||
"getTelegramFileTooLargeText": "Файл {fileName} больше {maxSizeMb} МБ и не может быть отправлен.",
|
||||
"getUserIsNowAdminText": "{name} теперь администратор!",
|
||||
"getUserAlreadyAdminText": "{name} уже администратор 🤔",
|
||||
"getUserNoLongerAdminText": "{name} больше не администратор!",
|
||||
"getUserWasNotAdminText": "{name} не был администратором 🤔",
|
||||
"getUserBannedText": "{name} забанен 🚫",
|
||||
"getUserBanFailedText": "Не удалось забанить {name} ☹️",
|
||||
"getUserUnbannedText": "{name} разбанен ⛓️💥",
|
||||
"getUserUnbanFailedText": "Не удалось разбанить {name} ☹️",
|
||||
"getUserIgnoredText": "{name} заглушён! 🔇",
|
||||
"getUserAlreadyIgnoredText": "{name} уже заглушён 🤔",
|
||||
"getUserIgnoreFailedText": "Не удалось заглушить {name} ☹️",
|
||||
"getUserUnignoredText": "{name} больше не заглушён! 🔈",
|
||||
"getUserWasNotIgnoredText": "{name} не был заглушён 🤔",
|
||||
"getUserUnignoreFailedText": "Не удалось включить {name} обратно ☹️",
|
||||
"getChoiceText": "Выбрал *{choice}*",
|
||||
"getCoinResultText": "Выпало: *{result}*",
|
||||
"getLoadedModelsText": "Загруженные модели: {models}",
|
||||
"getSelectedModelText": "Выбрана модель: `{model}`",
|
||||
"getCurrentModelText": "Текущая модель: `{model}`",
|
||||
"getLoadingModelText": "Загружаю модель `{model}`...",
|
||||
"getUserSettingsContextSizeText": "{size} токенов",
|
||||
"getQrCodeTextTooLongText": "Текст слишком длинный для QR ({actualLength} символов). Обрежу до {maxLength} символов.",
|
||||
"getQrCodeReadyText": "QR-код готов ✅\nСодержимое:\n<blockquote expandable>{content}</blockquote>",
|
||||
"getQrCodeFailedText": "Не удалось сгенерировать QR: {reason}",
|
||||
"getWhenPrefixText": "через ",
|
||||
"getWhenPluralUnitText": "{unit}",
|
||||
"getWhenDurationText": "{prefix}{value} {unit}",
|
||||
"commandDescriptions": {
|
||||
"ae": "вычисление",
|
||||
"adminsAdd": "Добавить пользователя в администраторы",
|
||||
"adminsRemove": "Удалить пользователя из администраторов",
|
||||
"ban": "забанить пользователя в чате",
|
||||
"choice": "Выбрать случайное значение",
|
||||
"coin": "Орёл или решка",
|
||||
"debug": "Вернуть msg или reply в JSON",
|
||||
"dice": "Отправить случайный или конкретный дайс",
|
||||
"distort": "Искажение изображения",
|
||||
"geminiChat": "Чат с AI (Gemini)",
|
||||
"geminiGetModel": "Показать текущую модель Gemini",
|
||||
"geminiListModels": "Показать все модели Gemini",
|
||||
"geminiSetModel": "Установить модель Gemini",
|
||||
"help": "Показать список команд",
|
||||
"id": "ID чата, пользователя и ответа",
|
||||
"ignore": "Бот будет игнорировать пользователя",
|
||||
"info": "Информация о боте",
|
||||
"leave": "Бот покинет текущий чат",
|
||||
"mistralChat": "Чат с AI (Mistral)",
|
||||
"mistralGetModel": "Показать текущую модель Mistral",
|
||||
"mistralListModels": "Показать все модели Mistral",
|
||||
"mistralSetModel": "Установить модель Mistral",
|
||||
"ollamaChat": "Чат с AI (Ollama)",
|
||||
"ollamaGetModel": "Показать текущую модель Ollama",
|
||||
"ollamaListModels": "Показать все модели Ollama",
|
||||
"ollamaSearch": "Веб-поиск через Ollama",
|
||||
"ollamaSetModel": "Установить модель Ollama",
|
||||
"openAiChat": "Чат с AI (OpenAI)",
|
||||
"openAiGetModel": "Показать текущую модель OpenAI",
|
||||
"openAiListModels": "Показать все модели OpenAI",
|
||||
"openAiSetModel": "Установить модель OpenAI",
|
||||
"ping": "Задержка между получением и отправкой сообщения",
|
||||
"qr": "Сгенерировать QR-код из текста",
|
||||
"quote": "Сделать цитату из текста",
|
||||
"randomInt": "Случайное число из диапазона",
|
||||
"randomString": "Случайная строка до 4096 символов",
|
||||
"settings": "Настройки пользователя",
|
||||
"shutdown": "Выключить бота",
|
||||
"speechToText": "Распознать речь в текст",
|
||||
"start": "Запустить бота",
|
||||
"systemInfo": "Информация о системе",
|
||||
"textToSpeech": "Сгенерировать речь из текста",
|
||||
"title": "Изменить название группы",
|
||||
"test": "Проверка системной функциональности",
|
||||
"transliteration": "Транслитерация EN <--> RU",
|
||||
"unban": "разбанить пользователя в чате",
|
||||
"unignore": "Бот снова будет отвечать пользователю",
|
||||
"uptime": "Время работы бота",
|
||||
"whatBetter": "случайно выбрать a или b",
|
||||
"when": "случайная дата"
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"language": {
|
||||
"default": "За замовчуванням",
|
||||
"en": "Англійська",
|
||||
"ru": "Російська",
|
||||
"ua": "Українська",
|
||||
"instructionName": "Ukrainian"
|
||||
},
|
||||
"providerChoice.default": "За замовчуванням",
|
||||
"errorText": "⚠️ Сталася помилка.",
|
||||
"waitThinkText": "⏳ Думаю...",
|
||||
"analyzingPictureText": "🔍 Аналізую зображення...",
|
||||
"analyzingPicturesText": "🔍 Аналізую зображення...",
|
||||
"reasoningText": "🤔 Міркую...",
|
||||
"transcribingAudioText": "🦻 Розпізнаю аудіо...",
|
||||
"genImageText": "👨🎨 Генерую зображення...",
|
||||
"cancelText": "❌ Скасувати",
|
||||
"regenerateText": "🔄 Згенерувати заново",
|
||||
"aiCancelCallbackText": "Скасувати генерацію AI",
|
||||
"aiRegenerateCallbackText": "Згенерувати відповідь AI заново",
|
||||
"userSettingsCallbackText": "Налаштування користувача",
|
||||
"noAccessText": "Немає доступу",
|
||||
"notBotCreatorText": "Ви не творець бота.",
|
||||
"notBotAdministratorText": "Ви не адміністратор бота.",
|
||||
"notAChatText": "Це не чат.",
|
||||
"notChatAdministratorText": "Ви не адміністратор чату.",
|
||||
"botNotChatAdministratorText": "Бот не є адміністратором чату.",
|
||||
"replyRequiredText": "Потрібно відповісти на повідомлення.",
|
||||
"onlyOriginalAuthorText": "Ця дія доступна лише автору початкового повідомлення.",
|
||||
"dockerContainerLabelText": "Docker-контейнер",
|
||||
"processLabelText": "Процес",
|
||||
"systemLabelText": "Система",
|
||||
"systemInfoOsLabelText": "ОС",
|
||||
"systemInfoRuntimeLabelText": "RUNTIME",
|
||||
"systemInfoDockerLabelText": "DOCKER",
|
||||
"systemInfoCpuLabelText": "CPU",
|
||||
"systemInfoRamLabelText": "RAM",
|
||||
"systemInfoCpuCoresText": "ядер",
|
||||
"systemInfoCpuThreadsText": "потоків",
|
||||
"idChatLabelText": "id чату",
|
||||
"idFromLabelText": "id користувача",
|
||||
"idReplyLabelText": "id відповіді",
|
||||
"runtimeProviderLabelText": "провайдер",
|
||||
"runtimeModelLabelText": "модель",
|
||||
"runtimeCapabilitiesLabelText": "можливості",
|
||||
"runtimeExternalLabelText": "зовнішній",
|
||||
"infoAiBlockLabelText": "AI",
|
||||
"infoSupportedProvidersLabelText": "підтримувані провайдери",
|
||||
"infoToolsBlockLabelText": "інструменти",
|
||||
"infoCountLabelText": "кількість",
|
||||
"infoCommandsBlockLabelText": "команди",
|
||||
"infoPublicLabelText": "публічні",
|
||||
"infoPrivateLabelText": "приватні",
|
||||
"infoChatLabelText": "чат",
|
||||
"infoCallbackLabelText": "колбеки",
|
||||
"commandsHeaderText": "Команди:\n\n",
|
||||
"sentCommandsInDmText": "Надіслав команди в особисті повідомлення 😎",
|
||||
"couldNotSendCommandsInDmText": "Не вдалося надіслати команди в особисті повідомлення ☹️\nНадсилаю їх сюди",
|
||||
"administratorsHeaderText": "*Адміністратори*:\n\n",
|
||||
"noUserInfoText": "Немає інформації про користувача",
|
||||
"useLeaveCommandText": "Використайте /leave",
|
||||
"databaseBackupCaption": "Резервна копія бази даних",
|
||||
"databaseBackupSentText": "Успішно надіслав творцю в особисті повідомлення!",
|
||||
"noChoicesText": "Немає з чого вибирати",
|
||||
"qrCodeMissingTextText": "Не знайдено текст для генерації QR-коду.",
|
||||
"quoteMissingTextText": "Не вдалося знайти текст у повідомленні 😢",
|
||||
"quoteBuildFailedText": "Не вдалося створити цитату 😢",
|
||||
"speechToTextInstructionText": "Надішліть аудіо/voice/video-note або відповідайте /stt на повідомлення з аудіо.",
|
||||
"speechToTextEmptyResultText": "Розпізнавання мовлення не повернуло текст.",
|
||||
"textToSpeechInstructionText": "Надішліть текст після команди або відповідайте /tts на повідомлення з текстом.",
|
||||
"titleMissingText": "Не вдалося знайти заголовок...",
|
||||
"betterFallbackText": "Краще",
|
||||
"pongText": "понг",
|
||||
"modelListHeaderText": "Доступні моделі:\n\n",
|
||||
"modelListLoadFailedText": "Не вдалося завантажити список моделей",
|
||||
"noCurrentModelText": "Модель не задана. Використайте одне зі значень зі списку.",
|
||||
"unsupportedAttachmentText": "Цей тип вкладення не підтримується.",
|
||||
"attachmentMissingFromCacheText": "Файл вкладення відсутній у кеші.",
|
||||
"noSupportedTranscriptionProviderText": "Не налаштовано жодного провайдера розпізнавання мовлення.",
|
||||
"noSupportedTextToSpeechProviderText": "Не налаштовано жодного провайдера синтезу мовлення.",
|
||||
"noSpeechToTextProviderForAccessText": "Для вашого рівня доступу не налаштовано провайдери розпізнавання мовлення.",
|
||||
"noTextToSpeechProviderForAccessText": "Для вашого рівня доступу не налаштовано провайдери синтезу мовлення.",
|
||||
"noTextToSynthesizeText": "Немає тексту для синтезу мовлення.",
|
||||
"speechFileTooLargeText": "Файл мовлення більший за 50 МБ і не може бути надісланий.",
|
||||
"userSettingsTitle": "Налаштування користувача",
|
||||
"userSettingsAiProviderSelectionTitle": "Вибір AI-провайдера",
|
||||
"userSettingsInterfaceLanguageSelectionTitle": "Вибір мови інтерфейсу",
|
||||
"userSettingsResponseLanguageSelectionTitle": "Вибір мови відповідей",
|
||||
"userSettingsContextSizeSelectionTitle": "Вибір розміру контексту",
|
||||
"userSettingsVoiceModeSelectionTitle": "Режим голосових повідомлень",
|
||||
"userSettingsTierLabel": "Рівень",
|
||||
"userSettingsAiProviderLabel": "AI-провайдер",
|
||||
"userSettingsInterfaceLanguageLabel": "Мова інтерфейсу",
|
||||
"userSettingsResponseLanguageLabel": "Мова відповідей LLM",
|
||||
"userSettingsContextSizeLabel": "Розмір контексту",
|
||||
"userSettingsVoiceModeLabel": "Голосові повідомлення",
|
||||
"userSettingsBackButtonText": "Назад",
|
||||
"userSettingsAiProviderButtonPrefix": "AI-провайдер",
|
||||
"userSettingsInterfaceLanguageButtonPrefix": "Мова інтерфейсу",
|
||||
"userSettingsResponseLanguageButtonPrefix": "Мова відповідей",
|
||||
"userSettingsContextSizeButtonPrefix": "Контекст",
|
||||
"userSettingsVoiceModeButtonPrefix": "Голосові",
|
||||
"userSettingsCreatorTierText": "Творець",
|
||||
"userSettingsAdminTierText": "Адмін",
|
||||
"userSettingsUserTierText": "Користувач",
|
||||
"userSettingsSelectedPrefix": "✓ ",
|
||||
"userSettingsContextSizeDefaultText": "За замовчуванням",
|
||||
"userSettingsVoiceModeExecuteText": "Виконувати через AI",
|
||||
"userSettingsVoiceModeTranscriptText": "Лише розшифровка",
|
||||
"startingImageGenText": "🌈 Запускаю генерацію зображення...",
|
||||
"imageGenText": "🌈 Генерую зображення...",
|
||||
"finalizingImageGenText": "🌈 Завершую генерацію зображення...",
|
||||
"botCannotMakeItselfAdminText": "Бот не може призначити себе адміністратором",
|
||||
"botCreatorAlreadyAdminText": "Творець бота вже адміністратор",
|
||||
"botCannotRemoveItselfFromAdminsText": "Бот не може видалити себе з адміністраторів",
|
||||
"botCreatorCannotStopBeingAdminText": "Творець бота не може перестати бути адміністратором",
|
||||
"botWillNotBanCreatorText": "Бот не банитиме свого творця.",
|
||||
"botWillNotBanAdminsText": "Бот не банитиме своїх адміністраторів.",
|
||||
"botIsNotBannedByItselfText": "Бот і так не забанений сам собою.",
|
||||
"botCreatorNeverBannedText": "Творець бота не забанений і ніколи не буде.",
|
||||
"botAdminsNotBannedText": "Адміністратори бота і так не забанені.",
|
||||
"botWillNotIgnoreItselfText": "Бот не ігноруватиме себе.",
|
||||
"botWillNotIgnoreCreatorText": "Бот не ігноруватиме свого творця.",
|
||||
"botWillNotIgnoreAdminsText": "Бот не ігноруватиме своїх адміністраторів.",
|
||||
"botIsNotIgnoredByItselfText": "Бот і так не ігнорує сам себе.",
|
||||
"botCreatorNotIgnoredText": "Творець бота не ігнорується і ніколи не буде.",
|
||||
"botAdminsNotIgnoredText": "Адміністратори бота і так не ігноруються.",
|
||||
"botAlreadyAlwaysListensToItselfText": "Бот і так завжди слухає сам себе",
|
||||
"botAlwaysListensToCreatorText": "Бот завжди слухає свого творця",
|
||||
"coinHeadsText": "Орел",
|
||||
"coinTailsText": "Решка",
|
||||
"distortReplyInstructionText": "Відповідайте /distort на повідомлення із зображенням (фото, документ або стікер).\nПриклад: /distort 16 80",
|
||||
"distortMissingImageText": "Не бачу зображення у відповіді. Надішліть фото або файл зображення.",
|
||||
"shutdownFallbackText": "...",
|
||||
"shutdownSequenceTexts": [
|
||||
"ну що ж, усі",
|
||||
"було приємно поспілкуватися",
|
||||
"але мені час відпочити",
|
||||
"усього доброго"
|
||||
],
|
||||
"shutdownDoneText": "*R.I.P*",
|
||||
"whenNowText": "прямо зараз",
|
||||
"whenNeverText": "ніколи",
|
||||
"whenYearUnitText": "рік",
|
||||
"whenDayUnitText": "день",
|
||||
"whenWeekUnitText": "тиждень",
|
||||
"whenMonthUnitText": "місяць",
|
||||
"whenHourUnitText": "година",
|
||||
"whenMinuteUnitText": "хвилина",
|
||||
"whenSecondUnitText": "секунда",
|
||||
"getCancelledText": "{provider}\n❌ Генерацію скасовано.",
|
||||
"getPartialImageGenText": "🌈 Генерую зображення ({iteration}/{total})...",
|
||||
"getImageGenDoneText.withModel": "👨🎨 Зображення згенеровано. Модель: `{model}`.",
|
||||
"getImageGenDoneText.default": "👨🎨 Зображення згенеровано.",
|
||||
"getErrorText.withReason": "{errorText} Причина:\n{reason}",
|
||||
"getUseToolText.python": "👨💻 Запускаю `Python`",
|
||||
"getUseToolText.default": "🔧 Використовую інструмент `{name}`",
|
||||
"getAnalyzingDocumentText.default": "🔍 Аналізую документ...",
|
||||
"getAnalyzingDocumentText.single": "🔍 Аналізую документ: `{name}`",
|
||||
"getAnalyzingDocumentText.many": "🔍 Аналізую документи: {names}",
|
||||
"getPreparingRAGText.default": "🔍 Готую RAG для документа...",
|
||||
"getPreparingRAGText.single": "🔍 Готую RAG для документа: `{name}`",
|
||||
"getPreparingRAGText.many": "🔍 Готую RAG для документів: {names}",
|
||||
"getBuildingRAGIndexText.default": "🧠 Будую RAG-індекс...",
|
||||
"getBuildingRAGIndexText.withModel": "🧠 Будую RAG-індекс: `{modelName}`.",
|
||||
"queueNoneText": "немає",
|
||||
"getAiQueueText.queued": "⏳ Запит до {provider} поставлено в чергу.",
|
||||
"getAiQueueText.ahead": "Запитів попереду: {count}.",
|
||||
"getTelegramFileTooLargeText": "Файл {fileName} більший за {maxSizeMb} МБ і не може бути надісланий.",
|
||||
"getUserIsNowAdminText": "{name} тепер адміністратор!",
|
||||
"getUserAlreadyAdminText": "{name} вже адміністратор 🤔",
|
||||
"getUserNoLongerAdminText": "{name} більше не адміністратор!",
|
||||
"getUserWasNotAdminText": "{name} не був адміністратором 🤔",
|
||||
"getUserBannedText": "{name} забанений 🚫",
|
||||
"getUserBanFailedText": "Не вдалося забанити {name} ☹️",
|
||||
"getUserUnbannedText": "{name} розбанений ⛓️💥",
|
||||
"getUserUnbanFailedText": "Не вдалося розбанити {name} ☹️",
|
||||
"getUserIgnoredText": "{name} заглушений! 🔇",
|
||||
"getUserAlreadyIgnoredText": "{name} вже заглушений 🤔",
|
||||
"getUserIgnoreFailedText": "Не вдалося заглушити {name} ☹️",
|
||||
"getUserUnignoredText": "{name} більше не заглушений! 🔈",
|
||||
"getUserWasNotIgnoredText": "{name} не був заглушений 🤔",
|
||||
"getUserUnignoreFailedText": "Не вдалося увімкнути {name} назад ☹️",
|
||||
"getChoiceText": "Вибрав *{choice}*",
|
||||
"getCoinResultText": "Випало: *{result}*",
|
||||
"getLoadedModelsText": "Завантажені моделі: {models}",
|
||||
"getSelectedModelText": "Обрано модель: `{model}`",
|
||||
"getCurrentModelText": "Поточна модель: `{model}`",
|
||||
"getLoadingModelText": "Завантажую модель `{model}`...",
|
||||
"getUserSettingsContextSizeText": "{size} токенів",
|
||||
"getQrCodeTextTooLongText": "Текст занадто довгий для QR ({actualLength} символів). Обріжу до {maxLength} символів.",
|
||||
"getQrCodeReadyText": "QR-код готовий ✅\nВміст:\n<blockquote expandable>{content}</blockquote>",
|
||||
"getQrCodeFailedText": "Не вдалося згенерувати QR: {reason}",
|
||||
"getWhenPrefixText": "через ",
|
||||
"getWhenPluralUnitText": "{unit}",
|
||||
"getWhenDurationText": "{prefix}{value} {unit}",
|
||||
"commandDescriptions": {
|
||||
"help": "Показати список команд",
|
||||
"settings": "Налаштування користувача",
|
||||
"start": "Запустити бота",
|
||||
"ping": "Затримка між отриманням і надсиланням повідомлення",
|
||||
"info": "Інформація про бота",
|
||||
"systemInfo": "Інформація про систему",
|
||||
"speechToText": "Розпізнати мовлення в текст",
|
||||
"textToSpeech": "Згенерувати мовлення з тексту",
|
||||
"qr": "Згенерувати QR-код з тексту",
|
||||
"quote": "Створити цитату з тексту",
|
||||
"choice": "Вибрати випадкове значення",
|
||||
"coin": "Орел або решка",
|
||||
"when": "випадкова дата"
|
||||
}
|
||||
}
|
||||
+913
-25
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,251 @@
|
||||
import {AsyncLocalStorage} from "node:async_hooks";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export const DEFAULT_LOCALE = "en";
|
||||
export const DEFAULT_LANGUAGE_CHOICE = "default";
|
||||
|
||||
export type LanguageChoice = string;
|
||||
export type LocalizationParam = string | number | boolean | null | undefined;
|
||||
export type LocalizationParams = Record<string, LocalizationParam>;
|
||||
type LocalizationBundle = Record<string, unknown>;
|
||||
|
||||
const KNOWN_LANGUAGE_ORDER = ["en", "ru", "ua"];
|
||||
|
||||
function normalizeLanguageCode(value: string | undefined | null): string | undefined {
|
||||
const normalized = value?.trim().toLowerCase().replace("_", "-");
|
||||
if (!normalized) return undefined;
|
||||
|
||||
const code = normalized.split("-")[0];
|
||||
return code === "uk" ? "ua" : code;
|
||||
}
|
||||
|
||||
function readMtimeMs(filePath: string): number | undefined {
|
||||
try {
|
||||
return fs.statSync(filePath).mtimeMs;
|
||||
} catch (e: any) {
|
||||
if (e?.code === "ENOENT") return undefined;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function valueByPath(bundle: LocalizationBundle, key: string): unknown {
|
||||
if (Object.prototype.hasOwnProperty.call(bundle, key)) {
|
||||
return bundle[key];
|
||||
}
|
||||
|
||||
return key.split(".").reduce<unknown>((value, part) => {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
return (value as Record<string, unknown>)[part];
|
||||
}, bundle);
|
||||
}
|
||||
|
||||
function interpolate(value: string, params: LocalizationParams): string {
|
||||
return value.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
|
||||
const param = params[key];
|
||||
return param === undefined || param === null ? match : String(param);
|
||||
});
|
||||
}
|
||||
|
||||
export class Localization {
|
||||
private static localesDir = path.resolve("locales");
|
||||
private static bundles = new Map<string, LocalizationBundle>();
|
||||
private static fileMtimeMs = new Map<string, number | undefined>();
|
||||
private static fileSignature = "";
|
||||
private static readonly storage = new AsyncLocalStorage<string>();
|
||||
|
||||
static configure(localesDir: string): void {
|
||||
Localization.localesDir = path.resolve(localesDir);
|
||||
Localization.reload(true);
|
||||
}
|
||||
|
||||
static reloadIfChanged(): void {
|
||||
Localization.reload(false);
|
||||
}
|
||||
|
||||
static runWithLocale<T>(locale: string, callback: () => T): T {
|
||||
const resolved = Localization.normalizeLocale(locale) ?? DEFAULT_LOCALE;
|
||||
return Localization.storage.run(resolved, callback);
|
||||
}
|
||||
|
||||
static currentLocale(): string {
|
||||
return Localization.storage.getStore() ?? DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
static resolveLocale(choice: LanguageChoice | undefined | null, telegramLanguageCode?: string): string {
|
||||
Localization.reloadIfChanged();
|
||||
|
||||
const normalizedChoice = Localization.normalizeLocale(choice);
|
||||
if (normalizedChoice && normalizedChoice !== DEFAULT_LANGUAGE_CHOICE && Localization.bundles.has(normalizedChoice)) {
|
||||
return normalizedChoice;
|
||||
}
|
||||
|
||||
const telegramLocale = Localization.normalizeLocale(telegramLanguageCode);
|
||||
if (telegramLocale && Localization.bundles.has(telegramLocale)) {
|
||||
return telegramLocale;
|
||||
}
|
||||
|
||||
return Localization.bundles.has(DEFAULT_LOCALE)
|
||||
? DEFAULT_LOCALE
|
||||
: Localization.availableLocaleCodes()[0] ?? DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
static normalizeLocale(value: LanguageChoice | undefined | null): string | undefined {
|
||||
return normalizeLanguageCode(value);
|
||||
}
|
||||
|
||||
static isKnownLanguageChoice(value: string | undefined | null): boolean {
|
||||
if (!value) return false;
|
||||
if (value === DEFAULT_LANGUAGE_CHOICE) return true;
|
||||
|
||||
const normalized = Localization.normalizeLocale(value);
|
||||
if (!normalized) return false;
|
||||
|
||||
Localization.reloadIfChanged();
|
||||
return Localization.bundles.has(normalized);
|
||||
}
|
||||
|
||||
static availableLocaleCodes(): string[] {
|
||||
Localization.reloadIfChanged();
|
||||
|
||||
return [...Localization.bundles.keys()].sort((a, b) => {
|
||||
const aIndex = KNOWN_LANGUAGE_ORDER.indexOf(a);
|
||||
const bIndex = KNOWN_LANGUAGE_ORDER.indexOf(b);
|
||||
|
||||
if (aIndex !== -1 || bIndex !== -1) {
|
||||
return (aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex)
|
||||
- (bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex);
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}
|
||||
|
||||
static languageChoices(): string[] {
|
||||
return [DEFAULT_LANGUAGE_CHOICE, ...Localization.availableLocaleCodes()];
|
||||
}
|
||||
|
||||
static languageLabel(choice: LanguageChoice): string {
|
||||
if (choice === DEFAULT_LANGUAGE_CHOICE) {
|
||||
return Localization.text("language.default", {}, "Default");
|
||||
}
|
||||
|
||||
const locale = Localization.normalizeLocale(choice) ?? choice;
|
||||
return Localization.text(`language.${locale}`, {}, locale.toUpperCase());
|
||||
}
|
||||
|
||||
static languageInstructionName(choice: LanguageChoice): string {
|
||||
if (choice === DEFAULT_LANGUAGE_CHOICE) return "";
|
||||
|
||||
const locale = Localization.normalizeLocale(choice) ?? choice;
|
||||
const bundle = Localization.bundles.get(locale);
|
||||
const value = bundle ? valueByPath(bundle, "language.instructionName") : undefined;
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : locale;
|
||||
}
|
||||
|
||||
static text(
|
||||
key: string,
|
||||
params: LocalizationParams = {},
|
||||
fallback = key,
|
||||
locale = Localization.currentLocale(),
|
||||
): string {
|
||||
Localization.reloadIfChanged();
|
||||
|
||||
const value = Localization.lookup(locale, key);
|
||||
return interpolate(typeof value === "string" ? value : fallback, params);
|
||||
}
|
||||
|
||||
static textArray(
|
||||
key: string,
|
||||
params: LocalizationParams = {},
|
||||
fallback: string[] = [],
|
||||
locale = Localization.currentLocale(),
|
||||
): string[] {
|
||||
Localization.reloadIfChanged();
|
||||
|
||||
const value = Localization.lookup(locale, key);
|
||||
const values = Array.isArray(value) && value.every(item => typeof item === "string")
|
||||
? value
|
||||
: fallback;
|
||||
|
||||
return values.map(item => interpolate(item, params));
|
||||
}
|
||||
|
||||
private static lookup(locale: string, key: string): unknown {
|
||||
const normalized = Localization.normalizeLocale(locale) ?? DEFAULT_LOCALE;
|
||||
const bundleValue = Localization.lookupInBundle(normalized, key);
|
||||
if (bundleValue !== undefined) return bundleValue;
|
||||
|
||||
if (normalized !== DEFAULT_LOCALE) {
|
||||
const fallbackValue = Localization.lookupInBundle(DEFAULT_LOCALE, key);
|
||||
if (fallbackValue !== undefined) return fallbackValue;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private static lookupInBundle(locale: string, key: string): unknown {
|
||||
const bundle = Localization.bundles.get(locale);
|
||||
return bundle ? valueByPath(bundle, key) : undefined;
|
||||
}
|
||||
|
||||
private static listLocaleFiles(): Map<string, string> {
|
||||
const files = new Map<string, string>();
|
||||
|
||||
if (!fs.existsSync(Localization.localesDir)) {
|
||||
return files;
|
||||
}
|
||||
|
||||
for (const entry of fs.readdirSync(Localization.localesDir, {withFileTypes: true})) {
|
||||
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
||||
|
||||
const locale = Localization.normalizeLocale(path.basename(entry.name, ".json"));
|
||||
if (locale) {
|
||||
files.set(locale, path.join(Localization.localesDir, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private static reload(force: boolean): void {
|
||||
try {
|
||||
const files = Localization.listLocaleFiles();
|
||||
const signature = [...files.entries()]
|
||||
.map(([locale, filePath]) => `${locale}:${filePath}`)
|
||||
.sort()
|
||||
.join("|");
|
||||
|
||||
const mtimes = new Map<string, number | undefined>();
|
||||
let changed = force || signature !== Localization.fileSignature;
|
||||
|
||||
for (const [locale, filePath] of files) {
|
||||
const mtimeMs = readMtimeMs(filePath);
|
||||
mtimes.set(locale, mtimeMs);
|
||||
|
||||
if (mtimeMs !== Localization.fileMtimeMs.get(locale)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
const bundles = new Map<string, LocalizationBundle>();
|
||||
for (const [locale, filePath] of files) {
|
||||
try {
|
||||
bundles.set(locale, JSON.parse(fs.readFileSync(filePath, "utf8")) as LocalizationBundle);
|
||||
} catch (e) {
|
||||
console.error(`Failed to load localization file ${filePath}`, e);
|
||||
const previous = Localization.bundles.get(locale);
|
||||
if (previous) bundles.set(locale, previous);
|
||||
}
|
||||
}
|
||||
|
||||
Localization.bundles = bundles;
|
||||
Localization.fileMtimeMs = mtimes;
|
||||
Localization.fileSignature = signature;
|
||||
} catch (e) {
|
||||
console.error("Failed to reload localization files", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user