Work on translation

This commit is contained in:
Yokayo
2025-01-12 00:42:58 +07:00
parent f462436450
commit 1d5cf8d25c
8 changed files with 188 additions and 77 deletions

View File

@ -2278,7 +2278,7 @@
<h4 data-i18n="Model Providers">Model Providers</h4> <h4 data-i18n="Model Providers">Model Providers</h4>
<select id="openrouter_providers_text" class="openrouter_providers" multiple> <select id="openrouter_providers_text" class="openrouter_providers" multiple>
</select> </select>
<label class="checkbox_label" for="openrouter_allow_fallbacks_textgenerationwebui" title="Automatically chooses an alternative provider if chosen providers can't serve your request."> <label class="checkbox_label" data-i18n="[title]Automatically chooses an alternative provider if chosen providers can't serve your request." for="openrouter_allow_fallbacks_textgenerationwebui" title="Automatically chooses an alternative provider if chosen providers can't serve your request.">
<input id="openrouter_allow_fallbacks_textgenerationwebui" type="checkbox" /> <input id="openrouter_allow_fallbacks_textgenerationwebui" type="checkbox" />
<span data-i18n="Allow fallback providers">Allow fallback providers</span> <span data-i18n="Allow fallback providers">Allow fallback providers</span>
</label> </label>
@ -2902,7 +2902,7 @@
<div> <div>
<h4 data-i18n="Window AI Model">Window AI Model</h4> <h4 data-i18n="Window AI Model">Window AI Model</h4>
<select id="model_windowai_select"> <select id="model_windowai_select">
<option value="">Use extension settings</option> <option data-i18n="Use extension settings" value="">Use extension settings</option>
<option value="openai/gpt-3.5-turbo">openai/gpt-3.5-turbo</option> <option value="openai/gpt-3.5-turbo">openai/gpt-3.5-turbo</option>
<option value="openai/gpt-3.5-turbo-16k">openai/gpt-3.5-turbo-16k</option> <option value="openai/gpt-3.5-turbo-16k">openai/gpt-3.5-turbo-16k</option>
<option value="openai/gpt-4">openai/gpt-4</option> <option value="openai/gpt-4">openai/gpt-4</option>
@ -2977,14 +2977,14 @@
<h4 data-i18n="Model Providers">Model Providers</h4> <h4 data-i18n="Model Providers">Model Providers</h4>
<select id="openrouter_providers_chat" class="openrouter_providers" multiple> <select id="openrouter_providers_chat" class="openrouter_providers" multiple>
</select> </select>
<label class="checkbox_label marginTopBot5" for="openrouter_allow_fallbacks" title="Automatically chooses an alternative provider if chosen providers can't serve your request."> <label class="checkbox_label marginTopBot5" for="openrouter_allow_fallbacks" data-i18n="[title]Automatically chooses an alternative provider if chosen providers can't serve your request." title="Automatically chooses an alternative provider if chosen providers can't serve your request.">
<input id="openrouter_allow_fallbacks" type="checkbox" /> <input id="openrouter_allow_fallbacks" type="checkbox" />
<span data-i18n="Allow fallback providers">Allow fallback providers</span> <span data-i18n="Allow fallback providers">Allow fallback providers</span>
</label> </label>
</div> </div>
<small class="marginTopBot5"> <small class="marginTopBot5">
<i class="fa-solid fa-lightbulb"></i> <i class="fa-solid fa-lightbulb"></i>
<span>To use instruct formatting, switch to OpenRouter under Text Completion API.</span> <span data-i18n="To use instruct formatting, switch to OpenRouter under Text Completion API.">To use instruct formatting, switch to OpenRouter under Text Completion API.</span>
</small> </small>
</form> </form>
<form id="scale_form" data-source="scale" action="javascript:void(null);" method="post" enctype="multipart/form-data"> <form id="scale_form" data-source="scale" action="javascript:void(null);" method="post" enctype="multipart/form-data">
@ -4601,7 +4601,7 @@
</div> </div>
</div> </div>
<div nane="AutoContiueBlock" class="inline-drawer wide100p flexFlowColumn"> <div nane="AutoContiueBlock" class="inline-drawer wide100p flexFlowColumn">
<div class="inline-drawer-toggle inline-drawer-header userSettingsInnerExpandable" title="Automatically 'continue' a response if the model stopped before reaching a certain amount of tokens."> <div class="inline-drawer-toggle inline-drawer-header userSettingsInnerExpandable" data-i18n="[title]Automatically 'continue' a response if the model stopped before reaching a certain amount of tokens." title="Automatically 'continue' a response if the model stopped before reaching a certain amount of tokens.">
<b><span data-i18n="Auto-Continue">Auto-Continue</span></b> <b><span data-i18n="Auto-Continue">Auto-Continue</span></b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div> </div>
@ -5734,7 +5734,7 @@
<span class="drag-handle">&#9776;</span> <span class="drag-handle">&#9776;</span>
<div class="gap5px world_entry_thin_controls wide100p alignitemscenter"> <div class="gap5px world_entry_thin_controls wide100p alignitemscenter">
<div class="inline-drawer-toggle fa-fw fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> <div class="inline-drawer-toggle fa-fw fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
<div class="fa-solid fa-toggle-on killSwitch" name="entryKillSwitch" title="Toggle entry's active state."></div> <div class="fa-solid fa-toggle-on killSwitch" name="entryKillSwitch" data-i18n="[title]Toggle entry's active state." title="Toggle entry's active state."></div>
<div class="flex-container alignitemscenter wide100p"> <div class="flex-container alignitemscenter wide100p">
<div class="WIEntryTitleAndStatus flex-container flex1 alignitemscenter"> <div class="WIEntryTitleAndStatus flex-container flex1 alignitemscenter">
<div class="flex-container flex1"> <div class="flex-container flex1">
@ -5967,7 +5967,7 @@
</small> </small>
</div> </div>
<div class="range-block-range"> <div class="range-block-range">
<input class="text_pole margin0" name="sticky" type="number" placeholder="Non-sticky" min="0" max="999999"> <input class="text_pole margin0" name="sticky" type="number" data-i18n="[placeholder]No-sticky" placeholder="Non-sticky" min="0" max="999999">
</div> </div>
</div> </div>
<div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Entries with a cooldown can't be activated N messages after being triggered." title="Entries with a cooldown can't be activated N messages after being triggered."> <div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Entries with a cooldown can't be activated N messages after being triggered." title="Entries with a cooldown can't be activated N messages after being triggered.">
@ -5980,7 +5980,7 @@
</small> </small>
</div> </div>
<div class="range-block-range"> <div class="range-block-range">
<input class="text_pole margin0" name="cooldown" type="number" placeholder="No cooldown" min="0" max="999999"> <input class="text_pole margin0" name="cooldown" type="number" data-i18n="[placeholder]No cooldown" placeholder="No cooldown" min="0" max="999999">
</div> </div>
</div> </div>
<div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Entries with a delay can't be activated until there are N messages present in the chat." title="Entries with a delay can't be activated until there are N messages present in the chat."> <div class="flex2 flex-container flexFlowColumn flexNoGap" data-i18n="[title]Entries with a delay can't be activated until there are N messages present in the chat." title="Entries with a delay can't be activated until there are N messages present in the chat.">
@ -5993,7 +5993,7 @@
</small> </small>
</div> </div>
<div class="range-block-range"> <div class="range-block-range">
<input class="text_pole margin0" name="delay" type="number" placeholder="No delay" min="0" max="999999"> <input class="text_pole margin0" name="delay" type="number" data-i18n="[placeholder]No delay" placeholder="No delay" min="0" max="999999">
</div> </div>
</div> </div>
</div> </div>

View File

@ -294,7 +294,7 @@
"Avoid sending sensitive information to the Horde.": "Избегайте отправки личной информации Horde", "Avoid sending sensitive information to the Horde.": "Избегайте отправки личной информации Horde",
"Review the Privacy statement": "Ознакомиться с заявлением о конфиденциальности", "Review the Privacy statement": "Ознакомиться с заявлением о конфиденциальности",
"Trusted workers only": "Только доверенные рабочие машины", "Trusted workers only": "Только доверенные рабочие машины",
"For privacy reasons, your API key will be hidden after you reload the page.": "По причинам безопасности ваш API-ключ будет скрыт после перезагрузки страницы.", "For privacy reasons, your API key will be hidden after you reload the page.": "Из соображений безопасности ваш API-ключ будет скрыт после перезагрузки страницы.",
"-- Horde models not loaded --": "--Модель Horde не загружена--", "-- Horde models not loaded --": "--Модель Horde не загружена--",
"Example: http://127.0.0.1:5000/api ": "Пример: http://127.0.0.1:5000/api", "Example: http://127.0.0.1:5000/api ": "Пример: http://127.0.0.1:5000/api",
"No connection...": "Нет соединения...", "No connection...": "Нет соединения...",
@ -331,7 +331,6 @@
"UID ↘": "UID ↘", "UID ↘": "UID ↘",
"Trigger% ↗": "Триггер% ↗", "Trigger% ↗": "Триггер% ↗",
"Trigger% ↘": "Триггер% ↘", "Trigger% ↘": "Триггер% ↘",
"Order:": "Порядок:",
"Depth:": "Глубина:", "Depth:": "Глубина:",
"Character Lore First": "Сначала лор персонажа", "Character Lore First": "Сначала лор персонажа",
"Global Lore First": "Сначала глобальный лор", "Global Lore First": "Сначала глобальный лор",
@ -347,7 +346,7 @@
"After Char Defs": "↓Перс.", "After Char Defs": "↓Перс.",
"Before AN": "↑АЗ", "Before AN": "↑АЗ",
"After AN": "↓АЗ", "After AN": "↓АЗ",
"Order": "Порядок:", "Order": "Очерёдность:",
"Update a theme file": "Обновить файл темы", "Update a theme file": "Обновить файл темы",
"Save as a new theme": "Сохранить как новую тему", "Save as a new theme": "Сохранить как новую тему",
"Minimum number of blacklisted words detected to trigger an auto-swipe": "Минимальное количество обнаруженных запрещённых слов, при котором срабатывает авто-свайп.", "Minimum number of blacklisted words detected to trigger an auto-swipe": "Минимальное количество обнаруженных запрещённых слов, при котором срабатывает авто-свайп.",
@ -497,7 +496,7 @@
"What this keyword should mean to the AI, sent verbatim": "Что это ключевое слово должно означать для ИИ, отправляется дословно", "What this keyword should mean to the AI, sent verbatim": "Что это ключевое слово должно означать для ИИ, отправляется дословно",
"Filter to Character(s)": "Фильтр по персонажу(ам)", "Filter to Character(s)": "Фильтр по персонажу(ам)",
"Character Exclusion": "Исключить персонажей", "Character Exclusion": "Исключить персонажей",
"Inclusion Group": "Включить персонажей", "Inclusion Group": "Группа записей",
"Only one entry with the same label will be activated": "Будет активна только одна запись с одинаковой меткой", "Only one entry with the same label will be activated": "Будет активна только одна запись с одинаковой меткой",
"-- Characters not found --": "-- Персонажей не найдено --", "-- Characters not found --": "-- Персонажей не найдено --",
"(This will be the first message from the character that starts every chat)": "(Это будет первое сообщение от персонажа, когда вы начинаете новый чат)", "(This will be the first message from the character that starts every chat)": "(Это будет первое сообщение от персонажа, когда вы начинаете новый чат)",
@ -541,7 +540,7 @@
"NOT ANY": "НЕ ЛЮБОЙ", "NOT ANY": "НЕ ЛЮБОЙ",
"Optional Filter": "Дополнительный фильтр", "Optional Filter": "Дополнительный фильтр",
"New Entry": "Новая запись", "New Entry": "Новая запись",
"Fill empty Memo/Titles with Keywords": "Заполните пустые Заметки/Названия ключевыми словами", "Fill empty Memo/Titles with Keywords": "Заполнить пустые названия ключевыми словами",
"AI Response Formatting": "Формат ответа ИИ", "AI Response Formatting": "Формат ответа ИИ",
"Change Background Image": "Изменить фон", "Change Background Image": "Изменить фон",
"Extensions": "Расширения", "Extensions": "Расширения",
@ -628,7 +627,7 @@
"UI Theme": "Тема UI", "UI Theme": "Тема UI",
"This message is invisible for the AI": "Это сообщение невидимо для ИИ", "This message is invisible for the AI": "Это сообщение невидимо для ИИ",
"Sampler Priority": "Приоритет сэмплеров", "Sampler Priority": "Приоритет сэмплеров",
"Ooba only. Determines the order of samplers.": "Только Ooba. Определяет порядок сэмплеров.", "Ooba only. Determines the order of samplers.": "Только oobabooga. Определяет порядок сэмплеров.",
"Load default order": "Загрузить стандартный порядок", "Load default order": "Загрузить стандартный порядок",
"Max Tokens Second": "Макс. кол-во токенов в секунду", "Max Tokens Second": "Макс. кол-во токенов в секунду",
"CFG": "CFG", "CFG": "CFG",
@ -1342,7 +1341,7 @@
"Chat Lorebook": "Chat Lorebook for", "Chat Lorebook": "Chat Lorebook for",
"chat_world_template_txt": "A selected World Info will be bound to this chat. When generating an AI reply,\n it will be combined with the entries from global and character lorebooks.", "chat_world_template_txt": "A selected World Info will be bound to this chat. When generating an AI reply,\n it will be combined with the entries from global and character lorebooks.",
"Use tag as folder": "Tag as folder", "Use tag as folder": "Tag as folder",
"WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized❌ Disabled": "Статус записи:\n 🔵 Постоянная\n 🟢 Обычная\n 🔗 Векторизованная\n ❌ Отключена", "WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized": "Статус записи:\n 🔵 Постоянная\n 🟢 Обычная\n 🔗 Векторизованная",
"WI_Entry_Status_Constant": "Постоянная", "WI_Entry_Status_Constant": "Постоянная",
"WI_Entry_Status_Normal": "Обычная", "WI_Entry_Status_Normal": "Обычная",
"WI_Entry_Status_Vectorized": "Векторизованная", "WI_Entry_Status_Vectorized": "Векторизованная",
@ -1366,13 +1365,7 @@
"Can be used to automatically activate Quick Replies": "Can be used to automatically activate Quick Replies", "Can be used to automatically activate Quick Replies": "Can be used to automatically activate Quick Replies",
"Automation ID": "Automation ID", "Automation ID": "Automation ID",
"( None )": "( None )", "( None )": "( None )",
"Delay until recursion (this entry can only be activated on recursive checking)": "Delay until recursion (this entry can only be activated on recursive checking)",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.\rSupports multiple comma-separated groups.\r\rDocumentation: World Info - Inclusion Group",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Prioritize this entry: When checked, this entry is prioritized out of all selections.\rIf multiple are prioritized, the one with the highest 'Order' is chosen.\r",
"A relative likelihood of entry activation within the group": "A relative likelihood of entry activation within the group",
"Group Weight": "Group Weight",
"Add Memo": "Add Memo", "Add Memo": "Add Memo",
"close": "close",
"reset": "reset", "reset": "reset",
"save": "save", "save": "save",
"Open checkpoint chat": "Открыть чат из чекпоинта", "Open checkpoint chat": "Открыть чат из чекпоинта",
@ -2035,5 +2028,121 @@
"tags_sorting_desc": "Автоматически сортировать теги по алфавиту после создания или переименования одного из них.\nПри выключении новые теги будут просто добавляться в конец.\n\nЕсли вручную перетащить один из тегов на другое место, автосортировка отключится.", "tags_sorting_desc": "Автоматически сортировать теги по алфавиту после создания или переименования одного из них.\nПри выключении новые теги будут просто добавляться в конец.\n\nЕсли вручную перетащить один из тегов на другое место, автосортировка отключится.",
"Imported tags:": "Импортируемые теги:", "Imported tags:": "Импортируемые теги:",
"Importing Tags": "Импорт тегов", "Importing Tags": "Импорт тегов",
"Couldn't import tags:": "Не удалось импортировать теги:" "Couldn't import tags:": "Не удалось импортировать теги:",
"Allow fallback models": "Разрешить fallback-модели",
"Allow fallback providers": "Разрешить fallback-провайдеров",
"To use instruct formatting, switch to OpenRouter under Text Completion API.": "Переключитесь на OpenRouter в Text Completion API, чтобы использовать форматирование Instruct-режима.",
"Select providers. No selection = all providers.": "Выберите провайдера. Нет выбранного = выбраны все.",
"Model Providers": "Провайдеры моделей",
"Select a model": "Выберите модель",
"Search models...": "Искать по моделям...",
"[Currently loaded]": "[Загруженная сейчас]",
"Search providers...": "Искать по провайдерам...",
"Automatically chooses an alternative provider if chosen providers can't serve your request.": "Автоматически переключаться на другого провайдера, если текущий не может обслужить запрос.",
"Example: 127.0.0.1:8000": "Пример: 127.0.0.1:8000",
"Edit a connection profile": "Редактировать профиль соединения",
"System Prompt Name": "Название системного промпта",
"Use System Prompt": "Использовать системный промпт",
"Hint:": "Подсказка:",
"Click on the setting name to omit it from the profile.": "Нажмите на название настройки, чтобы исключить её из профиля",
"Included settings:": "Сохранённые параметры:",
"Server URL": "Адрес сервера",
"NanoGPT API Key": "Ключ от API NanoGPT",
"NanoGPT Model": "Модель NanoGPT",
"Use extension settings": "Использовать настройки из расширения",
"DeepSeek API Key": "Ключ от API DeepSeek",
"DeepSeek Model": "Модель DeepSeek",
"prompt_post_processing_merge": "Объединять идущие подряд сообщения с одной ролью",
"prompt_post_processing_semi": "Semi-strict (чередовать роли)",
"prompt_post_processing_strict": "Strict (чередовать роли, сначала пользователь)",
"Select Horde models": "Выбрать модель из Horde",
"Model ID (optional)": "Идентификатор модели (необязательно)",
"Derive context size from backend": "Использовать бэкенд для определения размера контекста",
"Rename current preset": "Переименовать пресет",
"No Worlds active. Click here to select.": "Нет активных миров. Нажмите, чтобы выбрать.",
"Title/Memo": "Название",
"Strategy": "Статус",
"Position": "Позиция",
"Trigger %": "% срабатывания",
"Use global": "Глоб. настройка",
"Whole Words": "Целые слова",
"Non-recursable (will not be activated by another)": "Не рекурсивная (не активируется другими)",
"Delay until recursion (can only be activated on recursive checking)": "Рекурсивная (активируется только рекурсией)",
"Toggle entry's active state.": "Вкл/выкл запись.",
"Prioritize": "Важная",
"Prioritize this entry: When checked, this entry is prioritized out of all selections.If multiple are prioritized, the one with the highest 'Order' is chosen.": "Важная запись получает приоритет среди всех выбранных. Если важных записей несколько, выбирается та, у которой выше \"Очерёдность\".",
"Group Weight": "Вес в группе",
"A relative likelihood of entry activation within the group": "Относительная вероятность активации записи в рамках группы",
"Sticky": "Липучка",
"Sticky entries will stay active for N messages after being triggered.": "Запись-\"липучка\" останется активной в течение N сообщений после срабатывания.",
"Cooldown": "Кулдаун",
"Entries with a cooldown can't be activated N messages after being triggered.": "Запись с заданным кулдауном не активируется следующие N сообщений после срабатывания.",
"Delay": "Задержка",
"Entries with a delay can't be activated until there are N messages present in the chat.": "Запись с заданной задержкой может активироваться только после того, как в чате наберётся N сообщений.",
"No-sticky": "Нет",
"No cooldown": "Нет",
"No delay": "Нет",
"Filter to Characters or Tags": "Фильтровать по персонажам или тегам",
"Switch to plaintext mode": "Вкл/выкл режим чистого текста",
"Exclude": "Режим исключения",
"Switch the Character/Tags filter around to exclude the listed characters and tags from matching for this entry": "Инвертировать логику: для выбранных в фильтре персонажей/тегов данная запись активна НЕ БУДЕТ",
"Apply current sorting as Order": "Настроить Очерёдность в соответствии с текущей сортировкой",
"Create a new World Info": "Создать новый мир",
"Enter a name for the new file:": "Название нового файла:",
"Inclusion Groups ensure only one entry from a group is activated at a time, if multiple are triggered.Documentation: World Info - Inclusion Group": "Если сразу несколько записей из одной группы окажутся активированными, по факту сработает только одна. Одна запись может входить в несколько групп, отделяются запятыми. Раздел в документации: World Info - Inclusion Group",
"Valid World Info file name is required": "Требуется корректное имя для файла мира",
"World Info file has an invalid format": "Файл мира имеет неизвестный формат",
"World Info file has no entries": "В файле нет ни одной записи",
"Character not found.": "Персонаж не найден",
"Open a chat to get a name of the chat-bound lorebook": "Чтобы получить название привязанного к чату лорбука, требуется открыть чат.",
"File is not valid: ${0}": "Файл повреждён или имеет неизвестный формат: ${0}",
"The world with ${0} is invalid or corrupted.": "Мир ${0} повреждён или имеет неизвестный формат.",
"Deactivated all worlds": "Все миры отключены",
"No world found named: ${0}": "Не найдено мира с название ${0}",
"Activated world: ${0}": "Мир ${0} включён",
"Deactivated world: ${0}": "Мир ${0} отключён",
"World was not active: ${0}": "Мир ${0} не включён",
"The world '${0}' has been imported and linked to the character successfully.": "Мир ${0} успешно импортирован и привязан к персонажу.",
"World/Lorebook imported": "Мир/лорбук импортирован",
"Are you sure you want to import '${0}'?": "Вы точно хотите импортировать '${0}'?",
"It will overwrite the World/Lorebook with the same name.": "Существующий мир с таким же названием будет перезаписан.",
"Automatically 'continue' a response if the model stopped before reaching a certain amount of tokens.": "Автоматически \"продолжать\" ответ, если он оказался короче, чем целевая длина (в токенах).",
"None (not injected)": "Никуда",
"ext_sum_injection_position_none": "Пересказ не будет вставляться в промпт. Однако его по-прежнему можно будет вставить с помощью макроса {{summary}}",
"ext_sum_include_wi_scan": "Учитывать при сканировании лорбуком",
"ext_sum_include_wi_scan_desc": "Учитывать актуальный пересказ при сканировании промпта лорбуком.",
"ext_sum_force_tip": "Отправить запрос на создание пересказа прямо сейчас.",
"ext_sum_restore_tip": "Откатиться к предыдущему пересказу; используйте несколько раз, чтобы очистить историю пересказов для чата",
"Built-in Extensions:": "Комплектные расширения:",
"Installed Extensions:": "Установленные расширения:",
"Loading third-party extensions... Please wait...": "Загрузка сторонних расширений... Пожалуйста, подождите...",
"The page will be reloaded shortly...": "Страница будет перезагружена...",
"Extensions state changed": "Изменено состояние расширения",
"Error loading extensions. See browser console for details.": "Не удалось загрузить расширения. Подробности см. в консоли браузера.",
"You don't have permission to update global extensions.": "Отсутствуют права на обновление глобальных расширений.",
"Extension update failed": "Не удалось обновить расширение",
"Extension ${0} updated to ${1}": "Расширение ${0} обновлено до ${1}",
"Reload the page to apply updates": "Чтобы изменения вступили в силу, обновите страницу",
"You don't have permission to delete global extensions.": "Отсутствуют права на удаление глобальных расширений.",
"Are you sure you want to delete ${0}?": "Вы точно хотите удалить ${0}?",
"You don't have permission to move extensions.": "Отсутствуют права на перемещение расширений.",
"Are you sure you want to move ${0} to your local extensions? This will make it available only for you.": "Вы точно хотите сделать ${0} локальным расширением? После этого оно будет доступно только вам.",
"Are you sure you want to move ${0} to the global extensions? This will make it available for all users.": "Вы точно хотите сделать ${0} глобальным расширением? После этого оно будет доступно всем пользователям.",
"Extension ${0} moved.": "Расширение ${0} перемещено.",
"Extension ${0} deleted": "Расширение ${0} удалено.",
"Please wait...": "Пожалуйста, подождите...",
"Installing extension": "Идёт установка расширения",
"Extension installation failed": "Не удалось установить расширение",
"Extension '${0}' by ${1} (version ${2}) has been installed successfully!": "Расширение ${0} от автора ${1} (версия ${2}) успешно установлено!",
"Extension installation successful": "Расширение установлено",
"Extension updates available": "Доступных обновлений расширений",
"Auto-updating extensions. This may take several minutes.": "Запущено автоматическое обновление расширений. Это может занять несколько минут.",
"Install just for me": "Установить только мне",
"Install": "Установить",
"Install for all users": "Установить для всех пользователей",
"Modules provided by your Extras API:": "Модули из Extras API:",
"Not connected to the API!": "Нет соединения с API!",
"ext_type_system": "Это комплектное расширение. Его нельзя удалить, а обновляется оно вместе со всей системой.",
"Update all": "Обновить все",
"Close": "Закрыть"
} }

View File

@ -682,9 +682,9 @@ function getExtensionData(extension) {
* @return {string} - The HTML string for the module information. * @return {string} - The HTML string for the module information.
*/ */
function getModuleInformation() { function getModuleInformation() {
let moduleInfo = modules.length ? `<p>${DOMPurify.sanitize(modules.join(', '))}</p>` : '<p class="failure">Not connected to the API!</p>'; let moduleInfo = modules.length ? `<p>${DOMPurify.sanitize(modules.join(', '))}</p>` : '<p class="failure">' + t`Not connected to the API!` + '</p>';
return ` return `
<h3>Modules provided by your Extras API:</h3> <h3>` + t`Modules provided by your Extras API:` + `</h3>
${moduleInfo} ${moduleInfo}
`; `;
} }
@ -703,11 +703,11 @@ async function showExtensionsDetails() {
initialScrollTop = oldPopup.content.scrollTop; initialScrollTop = oldPopup.content.scrollTop;
await oldPopup.completeCancelled(); await oldPopup.completeCancelled();
} }
const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">Built-in Extensions:</h3></div>'); const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Built-in Extensions:` + '</h3></div>');
const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">Installed Extensions:</h3></div>'); const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Installed Extensions:` + '</h3></div>');
const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5"> const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
<i class="fa-solid fa-spinner fa-spin"></i> <i class="fa-solid fa-spinner fa-spin"></i>
<span>Loading third-party extensions... Please wait...</span> <span>` + t`Loading third-party extensions... Please wait...` + `</span>
</div>`); </div>`);
htmlExternal.append(htmlLoading); htmlExternal.append(htmlLoading);
@ -728,7 +728,7 @@ async function showExtensionsDetails() {
/** @type {import('./popup.js').CustomPopupButton} */ /** @type {import('./popup.js').CustomPopupButton} */
const updateAllButton = { const updateAllButton = {
text: 'Update all', text: t`Update all`,
appendAtEnd: true, appendAtEnd: true,
action: async () => { action: async () => {
requiresReload = true; requiresReload = true;
@ -740,7 +740,7 @@ async function showExtensionsDetails() {
let waitingForSave = false; let waitingForSave = false;
const popup = new Popup(html, POPUP_TYPE.TEXT, '', { const popup = new Popup(html, POPUP_TYPE.TEXT, '', {
okButton: 'Close', okButton: t`Close`,
wide: true, wide: true,
large: true, large: true,
customButtons: [updateAllButton], customButtons: [updateAllButton],
@ -833,7 +833,7 @@ async function updateExtension(extensionName, quiet) {
toastr.success('Extension is already up to date'); toastr.success('Extension is already up to date');
} }
} else { } else {
toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`, 'Reload the page to apply updates'); toastr.success(t`Extension ${extensionName} updated to ${data.shortCommitHash}`, t`Reload the page to apply updates`);
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
@ -1001,7 +1001,7 @@ export async function installExtension(url, global) {
} }
const response = await request.json(); const response = await request.json();
toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been installed successfully!`, 'Extension installation successful'); toastr.success(t`Extension '${response.display_name}' by ${response.author} (version ${response.version}) has been installed successfully!`, t`Extension installation successful`);
console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`); console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
await loadExtensionSettings({}, false, false); await loadExtensionSettings({}, false, false);
await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED); await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
@ -1175,7 +1175,7 @@ async function checkForExtensionUpdates(force) {
await Promise.allSettled(promises); await Promise.allSettled(promises);
if (updatesAvailable.length > 0) { if (updatesAvailable.length > 0) {
toastr.info(`${updatesAvailable.map(x => `${x}`).join('\n')}`, 'Extension updates available'); toastr.info(`${updatesAvailable.map(x => `${x}`).join('\n')}`, t`Extension updates available`);
} }
} }
@ -1189,7 +1189,7 @@ async function autoUpdateExtensions(forceAll) {
return; return;
} }
const banner = toastr.info('Auto-updating extensions. This may take several minutes.', 'Please wait...', { timeOut: 10000, extendedTimeOut: 10000 }); const banner = toastr.info(t`Auto-updating extensions. This may take several minutes.`, t`Please wait...`, { timeOut: 10000, extendedTimeOut: 10000 });
const isCurrentUserAdmin = isAdmin(); const isCurrentUserAdmin = isAdmin();
const promises = []; const promises = [];
for (const [id, manifest] of Object.entries(manifests)) { for (const [id, manifest] of Object.entries(manifests)) {

View File

@ -1,10 +1,10 @@
<div> <div>
<h3>Included settings:</h3> <h3 data-i18n="Included settings:">Included settings:</h3>
<div class="justifyLeft flex-container flexFlowColumn flexNoGap"> <div class="justifyLeft flex-container flexFlowColumn flexNoGap">
{{#each settings}} {{#each settings}}
<label class="checkbox_label"> <label class="checkbox_label">
<input type="checkbox" value="{{@key}}" name="exclude"{{#if this}} checked{{/if}}> <input type="checkbox" value="{{@key}}" name="exclude"{{#if this}} checked{{/if}}>
<span>{{@key}}</span> <span data-i18n="{{@key}}">{{@key}}</span>
</label> </label>
{{/each}} {{/each}}
</div> </div>

View File

@ -12,8 +12,8 @@
</div> </div>
<div class="marginTop5"> <div class="marginTop5">
<small> <small>
<b>Hint:</b> <b data-i18n="Hint:">Hint:</b>
<i>Click on the setting name to omit it from the profile.</i> <i data-i18n="Click on the setting name to omit it from the profile.">Click on the setting name to omit it from the profile.</i>
</small> </small>
</div> </div>
<h3 data-i18n="Enter a name:"> <h3 data-i18n="Enter a name:">

View File

@ -5491,8 +5491,8 @@ export function initOpenAI() {
if (!isMobile()) { if (!isMobile()) {
$('#model_openrouter_select').select2({ $('#model_openrouter_select').select2({
placeholder: 'Select a model', placeholder: t`Select a model`,
searchInputPlaceholder: 'Search models...', searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
templateResult: getOpenRouterModelTemplate, templateResult: getOpenRouterModelTemplate,

View File

@ -5,6 +5,7 @@ import { textgenerationwebui_settings as textgen_settings, textgen_types } from
import { tokenizers } from './tokenizers.js'; import { tokenizers } from './tokenizers.js';
import { renderTemplateAsync } from './templates.js'; import { renderTemplateAsync } from './templates.js';
import { POPUP_TYPE, callGenericPopup } from './popup.js'; import { POPUP_TYPE, callGenericPopup } from './popup.js';
import { t } from './i18n.js';
let mancerModels = []; let mancerModels = [];
let togetherModels = []; let togetherModels = [];
@ -936,71 +937,71 @@ export function initTextGenModels() {
if (!isMobile()) { if (!isMobile()) {
$('#mancer_model').select2({ $('#mancer_model').select2({
placeholder: 'Select a model', placeholder: t`Select a model`,
searchInputPlaceholder: 'Search models...', searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
templateResult: getMancerModelTemplate, templateResult: getMancerModelTemplate,
}); });
$('#model_togetherai_select').select2({ $('#model_togetherai_select').select2({
placeholder: 'Select a model', placeholder: t`Select a model`,
searchInputPlaceholder: 'Search models...', searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
templateResult: getTogetherModelTemplate, templateResult: getTogetherModelTemplate,
}); });
$('#ollama_model').select2({ $('#ollama_model').select2({
placeholder: 'Select a model', placeholder: t`Select a model`,
searchInputPlaceholder: 'Search models...', searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
}); });
$('#tabby_model').select2({ $('#tabby_model').select2({
placeholder: '[Currently loaded]', placeholder: t`[Currently loaded]`,
searchInputPlaceholder: 'Search models...', searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
allowClear: true, allowClear: true,
}); });
$('#model_infermaticai_select').select2({ $('#model_infermaticai_select').select2({
placeholder: 'Select a model', placeholder: t`Select a model`,
searchInputPlaceholder: 'Search models...', searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
templateResult: getInfermaticAIModelTemplate, templateResult: getInfermaticAIModelTemplate,
}); });
$('#model_dreamgen_select').select2({ $('#model_dreamgen_select').select2({
placeholder: 'Select a model', placeholder: t`Select a model`,
searchInputPlaceholder: 'Search models...', searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
templateResult: getDreamGenModelTemplate, templateResult: getDreamGenModelTemplate,
}); });
$('#openrouter_model').select2({ $('#openrouter_model').select2({
placeholder: 'Select a model', placeholder: t`Select a model`,
searchInputPlaceholder: 'Search models...', searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
templateResult: getOpenRouterModelTemplate, templateResult: getOpenRouterModelTemplate,
}); });
$('#vllm_model').select2({ $('#vllm_model').select2({
placeholder: 'Select a model', placeholder: t`Select a model`,
searchInputPlaceholder: 'Search models...', searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
templateResult: getVllmModelTemplate, templateResult: getVllmModelTemplate,
}); });
$('#aphrodite_model').select2({ $('#aphrodite_model').select2({
placeholder: 'Select a model', placeholder: t`Select a model`,
searchInputPlaceholder: 'Search models...', searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
templateResult: getAphroditeModelTemplate, templateResult: getAphroditeModelTemplate,
}); });
providersSelect.select2({ providersSelect.select2({
sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)), sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)),
placeholder: 'Select providers. No selection = all providers.', placeholder: t`Select providers. No selection = all providers.`,
searchInputPlaceholder: 'Search providers...', searchInputPlaceholder: t`Search providers...`,
searchInputCssClass: 'text_pole', searchInputCssClass: 'text_pole',
width: '100%', width: '100%',
closeOnSelect: false, closeOnSelect: false,

View File

@ -20,6 +20,7 @@ import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
import { StructuredCloneMap } from './util/StructuredCloneMap.js'; import { StructuredCloneMap } from './util/StructuredCloneMap.js';
import { renderTemplateAsync } from './templates.js'; import { renderTemplateAsync } from './templates.js';
import { t } from './i18n.js';
export const world_info_insertion_strategy = { export const world_info_insertion_strategy = {
evenly: 0, evenly: 0,
@ -909,21 +910,21 @@ function registerWorldInfoSlashCommands() {
async function getEntriesFromFile(file) { async function getEntriesFromFile(file) {
if (!file || !world_names.includes(file)) { if (!file || !world_names.includes(file)) {
toastr.warning('Valid World Info file name is required'); toastr.warning(t`Valid World Info file name is required`);
return ''; return '';
} }
const data = await loadWorldInfo(file); const data = await loadWorldInfo(file);
if (!data || !('entries' in data)) { if (!data || !('entries' in data)) {
toastr.warning('World Info file has an invalid format'); toastr.warning(t`World Info file has an invalid format`);
return ''; return '';
} }
const entries = Object.values(data.entries); const entries = Object.values(data.entries);
if (!entries || entries.length === 0) { if (!entries || entries.length === 0) {
toastr.warning('World Info file has no entries'); toastr.warning(t`World Info file has no entries`);
return ''; return '';
} }
@ -951,7 +952,7 @@ function registerWorldInfoSlashCommands() {
name = String(name ?? '') || context.characters[context.characterId]?.avatar || null; name = String(name ?? '') || context.characters[context.characterId]?.avatar || null;
const character = findChar({ name }); const character = findChar({ name });
if (!character) { if (!character) {
toastr.error('Character not found.'); toastr.error(t`Character not found.`);
return ''; return '';
} }
const books = []; const books = [];
@ -977,7 +978,7 @@ function registerWorldInfoSlashCommands() {
const chatId = getCurrentChatId(); const chatId = getCurrentChatId();
if (!chatId) { if (!chatId) {
toastr.warning('Open a chat to get a name of the chat-bound lorebook'); toastr.warning(t`Open a chat to get a name of the chat-bound lorebook`);
return ''; return '';
} }
@ -4773,7 +4774,7 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
const bookName = characters[chid]?.data?.character_book?.name || `${characters[chid]?.name}'s Lorebook`; const bookName = characters[chid]?.data?.character_book?.name || `${characters[chid]?.name}'s Lorebook`;
if (!skipPopup) { if (!skipPopup) {
const confirmation = await Popup.show.confirm(`Are you sure you want to import "${bookName}"?`, world_names.includes(bookName) ? 'It will overwrite the World/Lorebook with the same name.' : ''); const confirmation = await Popup.show.confirm(t`Are you sure you want to import '${bookName}'?`, world_names.includes(bookName) ? t`It will overwrite the World/Lorebook with the same name.` : '');
if (!confirmation) { if (!confirmation) {
return; return;
} }
@ -4785,7 +4786,7 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
await updateWorldInfoList(); await updateWorldInfoList();
$('#character_world').val(bookName).trigger('change'); $('#character_world').val(bookName).trigger('change');
toastr.success(`The world "${bookName}" has been imported and linked to the character successfully.`, 'World/Lorebook imported'); toastr.success(t`The world '${bookName}' has been imported and linked to the character successfully.`, t`World/Lorebook imported`);
const newIndex = world_names.indexOf(bookName); const newIndex = world_names.indexOf(bookName);
if (newIndex >= 0) { if (newIndex >= 0) {
@ -4813,9 +4814,9 @@ export function onWorldInfoChange(args, text) {
if (selected_world_info.includes(name)) { if (selected_world_info.includes(name)) {
selected_world_info.splice(selected_world_info.indexOf(name), 1); selected_world_info.splice(selected_world_info.indexOf(name), 1);
wiElement.prop('selected', false); wiElement.prop('selected', false);
if (!silent) toastr.success(`Deactivated world: ${name}`); if (!silent) toastr.success(t`Deactivated world: ${name}`);
} else { } else {
if (!silent) toastr.error(`World was not active: ${name}`); if (!silent) toastr.error(t`World was not active: ${name}`);
} }
break; break;
} }
@ -4823,11 +4824,11 @@ export function onWorldInfoChange(args, text) {
if (selected_world_info.includes(name)) { if (selected_world_info.includes(name)) {
selected_world_info.splice(selected_world_info.indexOf(name), 1); selected_world_info.splice(selected_world_info.indexOf(name), 1);
wiElement.prop('selected', false); wiElement.prop('selected', false);
if (!silent) toastr.success(`Deactivated world: ${name}`); if (!silent) toastr.success(t`Deactivated world: ${name}`);
} else { } else {
selected_world_info.push(name); selected_world_info.push(name);
wiElement.prop('selected', true); wiElement.prop('selected', true);
if (!silent) toastr.success(`Activated world: ${name}`); if (!silent) toastr.success(t`Activated world: ${name}`);
} }
break; break;
} }
@ -4835,16 +4836,16 @@ export function onWorldInfoChange(args, text) {
default: { default: {
selected_world_info.push(name); selected_world_info.push(name);
wiElement.prop('selected', true); wiElement.prop('selected', true);
if (!silent) toastr.success(`Activated world: ${name}`); if (!silent) toastr.success(t`Activated world: ${name}`);
} }
} }
} else { } else {
if (!silent) toastr.error(`No world found named: ${worldName}`); if (!silent) toastr.error(t`No world found named: ${worldName}`);
} }
}); });
$('#world_info').trigger('change'); $('#world_info').trigger('change');
} else { // if no args, unset all worlds } else { // if no args, unset all worlds
if (!silent) toastr.success('Deactivated all worlds'); if (!silent) toastr.success(t`Deactivated all worlds`);
selected_world_info = []; selected_world_info = [];
$('#world_info').val(null).trigger('change'); $('#world_info').val(null).trigger('change');
} }
@ -4860,7 +4861,7 @@ export function onWorldInfoChange(args, text) {
} else { } else {
const wiElement = getWIElement(existingWorldName); const wiElement = getWIElement(existingWorldName);
wiElement.prop('selected', false); wiElement.prop('selected', false);
toastr.error(`The world with ${existingWorldName} is invalid or corrupted.`); toastr.error(t`The world with ${existingWorldName} is invalid or corrupted.`);
} }
}); });
} }
@ -4892,7 +4893,7 @@ export async function importWorldInfo(file) {
} }
if (jsonData === undefined || jsonData === null) { if (jsonData === undefined || jsonData === null) {
toastr.error(`File is not valid: ${file.name}`); toastr.error(t`File is not valid: ${file.name}`);
return; return;
} }
@ -5038,7 +5039,7 @@ jQuery(() => {
$('#world_create_button').on('click', async () => { $('#world_create_button').on('click', async () => {
const tempName = getFreeWorldName(); const tempName = getFreeWorldName();
const finalName = await Popup.show.input('Create a new World Info', 'Enter a name for the new file:', tempName); const finalName = await Popup.show.input(t`Create a new World Info`, t`Enter a name for the new file:`, tempName);
if (finalName) { if (finalName) {
await createNewWorldInfo(finalName, { interactive: true }); await createNewWorldInfo(finalName, { interactive: true });