diff --git a/public/css/rm-groups.css b/public/css/rm-groups.css index 499f59873..6e12164b2 100644 --- a/public/css/rm-groups.css +++ b/public/css/rm-groups.css @@ -87,7 +87,7 @@ } #rm_group_members:empty::before { - content: 'Group is empty'; + content: attr(group_empty_text); font-weight: bolder; width: 100%; @@ -115,7 +115,7 @@ } #rm_group_add_members:empty::before { - content: 'No characters available'; + content: attr(no_characters_text); font-weight: bolder; width: 100%; diff --git a/public/index.html b/public/index.html index f3b04f1bc..c8b671eaa 100644 --- a/public/index.html +++ b/public/index.html @@ -2052,8 +2052,8 @@
@@ -5531,7 +5531,7 @@
-
+
@@ -5549,7 +5549,7 @@
-
+
@@ -5969,7 +5969,7 @@
-
 entries
+
 entries
@@ -6704,7 +6704,7 @@
- Go back + Go back
diff --git a/public/locales/ru-ru.json b/public/locales/ru-ru.json index c1591f212..44db7987d 100644 --- a/public/locales/ru-ru.json +++ b/public/locales/ru-ru.json @@ -52,7 +52,7 @@ "Presence Penalty": "Штраф за присутствие", "Top A": "Top А", "Tail Free Sampling": "Tail Free Sampling", - "Rep. Pen. Slope": "Rep. Pen. Slope", + "Rep. Pen. Slope": "Рост штрафа за повтор к концу промпта", "Top K": "Top K", "Top P": "Top P", "Do Sample": "Включить сэмплинг", @@ -162,9 +162,9 @@ "Story String": "Строка истории", "Example Separator": "Разделитель примеров сообщений", "Chat Start": "Начало чата", - "Activation Regex": "Regex для активации", + "Activation Regex": "Рег. выражение для активации", "Instruct Mode": "Режим Instruct", - "Wrap Sequences with Newline": "Отделять строки символом новой строки", + "Wrap Sequences with Newline": "Каждая строка из шаблона на новой строке", "Include Names": "Добавлять имена", "Force for Groups and Personas": "Также для групп и персон", "System Prompt": "Системный промпт", @@ -299,7 +299,7 @@ "AI Horde": "AI Horde", "NovelAI": "NovelAI", "OpenAI API key": "Ключ для API OpenAI", - "Trim spaces": "Обрезать пробелы", + "Trim spaces": "Обрезать пробелы в начале и конце", "Trim Incomplete Sentences": "Удалять неоконченные предложения", "Include Newline": "Добавлять новую строку", "Non-markdown strings": "Строки без разметки", @@ -510,7 +510,7 @@ "New preset": "Новый пресет", "Delete preset": "Удалить пресет", "API Connections": "Соединения с API", - "Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "Может помочь с плохими ответами ставя в очередь только подтвержденных работников. Может замедлить время ответа.", + "Can help with bad responses by queueing only the approved workers. May slowdown the response time.": "Может помочь при плохих ответах, делая запросы только к доверенным рабочим машинам. Может замедлить время ответа.", "Clear your API key": "Стереть ключ от API", "Refresh models": "Обновить модели", "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Получите свой OpenRouter API токен используя OAuth. У вас будет открыта вкладка openrouter.ai", @@ -551,7 +551,7 @@ "Token counts may be inaccurate and provided just for reference.": "Счетчик токенов может быть неточным, используйте как ориентир", "Click to select a new avatar for this character": "Нажмите чтобы выбрать новый аватар для этого персонажа", "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "Пример:\n [{{user}} is a 28-year-old Romanian cat girl.]", - "Toggle grid view": "Переключить вид сетки", + "Toggle grid view": "Сменить вид сетки", "Add to Favorites": "Добавить в Избранное", "Advanced Definition": "Расширенное описание", "Character Lore": "Лор персонажа", @@ -624,7 +624,7 @@ "UI Theme": "Тема UI", "This message is invisible for the AI": "Это сообщение невидимо для ИИ", "Sampler Priority": "Приоритет сэмплеров", - "Ooba only. Determines the order of samplers.": "Только oobabooga. Определяет порядок сэмплеров.", + "Ooba only. Determines the order of samplers.": "Только для oobabooga. Определяет порядок сэмплеров.", "Load default order": "Загрузить стандартный порядок", "Max Tokens Second": "Макс. кол-во токенов в секунду", "CFG": "CFG", @@ -695,7 +695,7 @@ "Medium": "Средний", "Aggressive": "Агрессивный", "Very aggressive": "Очень агрессивный", - "Eta_Cutoff_desc": "Eta cutoff - основной параметр специальной техники сэмплинга под названием Eta Sampling. В единицах 1e-4; разумное значение - 3. Установите в 0, чтобы отключить. См. статью Truncation Sampling as Language Model Desmoothing от Хьюитт и др. (2022) для получения подробной информации.", + "Eta_Cutoff_desc": "Eta cutoff - основной параметр специальной техники сэмплинга под названием Eta Sampling.\nВ единицах 1e-4; разумное значение - 3.\nУстановите в 0, чтобы отключить.\nСм. статью Truncation Sampling as Language Model Desmoothing от Хьюитт и др. (2022) для получения подробной информации.", "Learn how to contribute your idle GPU cycles to the Horde": "Узнайте, как использовать время простоя вашего GPU для помощи Horde", "Use the appropriate tokenizer for Google models via their API. Slower prompt processing, but offers much more accurate token counting.": "Используйте соответствующий токенизатор для моделей Google через их API. Медленная обработка подсказок, но предлагает намного более точный подсчет токенов.", "Load koboldcpp order": "Загрузить порядок из koboldcpp", @@ -964,7 +964,7 @@ "char_import_3": "Персонаж с JanitorAI (прямая ссылка или UUID)", "char_import_4": "Персонаж с Pygmalion.chat (прямая ссылка или UUID)", "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)", - "char_import_6": "Прямая ссылка на PNG-файл (чтобы узнать список разрешённых хостов, загляните в", + "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в", "char_import_7": ")", "Grammar String": "Грамматика", "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.", @@ -1016,7 +1016,7 @@ "prompt_manager_relative": "Относительная", "prompt_manager_depth": "Глубина", "Injection depth. 0 = after the last message, 1 = before the last message, etc.": "Глубина вставки. 0 = после последнего сообщения, 1 = перед последним сообщением, и т.д.", - "The prompt to be sent.": "Отправляемый ИИ промпт.", + "The prompt to be sent.": "Текст промпта.", "prompt_manager_forbid_overrides": "Запретить перезапись", "This prompt cannot be overridden by character cards, even if overrides are preferred.": "Карточка персонажа не сможет перезаписать этот промпт, даже если настройки отдают приоритет именно ей.", "image_inlining_hint_1": "Отправлять картинки как часть промпта, если позволяет модель (такой функционал поддерживают GPT-4V, Claude 3 или Llava 13B). Чтобы добавить в чат изображение, используйте на нужном сообщении действие", @@ -1232,7 +1232,7 @@ "Top P & Min P": "Top P & Min P", "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.": "llama.cpp only. Determines the order of samplers. If Mirostat mode is not 0, sampler order is ignored.", "Helps the model to associate messages with characters.": "Помогает модели связывать сообщения с персонажами.", - "character_names_default": "Except for groups and past personas. Otherwise, make sure you provide names in the prompt.", + "character_names_default": "Добавлять префиксы для групповых чатов и предыдущих персон. В остальных случаях указывайте имена в промпте иными способами.", "Completion": "Completion Object", "character_names_completion": "Только латинские буквы, цифры и знак подчёркивания. Работает не для всех бэкендов, в частности для Claude, MistralAI, Google.", "Use AI21 Tokenizer": "Использовать токенайзер AI21", @@ -1277,7 +1277,7 @@ "Will be inserted as a last prompt line when using system/neutral generation.": "Will be inserted as a last prompt line when using system/neutral generation.", "If a stop sequence is generated, everything past it will be removed from the output (inclusive).": "Если ИИ генерирует стоп-строку, то всё после неё будет вырезано из ответа (включая и саму стоп-строку).", "Will be inserted at the start of the chat history if it doesn't start with a User message.": "Вставляется в начале истории чата, если она начинается не с сообщения пользователя.", - "Global World Info/Lorebook activation settings": "Настройки активации глобального лорбука / Информации о мире", + "Global World Info/Lorebook activation settings": "Глобальные настройки активации лорбука / Информации о мире", "Click to expand": "Щёлкните, чтобы развернуть", "Insertion Strategy": "Как инжектить", "Only the entries with the most number of key matches will be selected for Inclusion Group filtering": "Only the entries with the most number of key matches will be selected for Inclusion Group filtering", @@ -1646,8 +1646,8 @@ "mui_reset": "Сброс", "Quick 'Impersonate' button": "Быстрое перевоплощение", "Show a button in the input area to ask the AI to impersonate your character for a single message": "Показать в поле ввода кнопку, по нажатии на которую ИИ сгенерирует одно сообщение от лица вашего персонажа.", - "Separators as Stop Strings": "Разделители как стоп-строки", - "Names as Stop Strings": "Имена как стоп-строки", + "Separators as Stop Strings": "Разделители в качестве стоп-строк", + "Names as Stop Strings": "Имена в качестве стоп-строк", "Add Character and User names to a list of stopping strings.": "Добавлять имена персонажа и пользователя в список стоп-строк.", "context_allow_post_history_instructions": "Добавлять в конец промпта инструкции после истории. Работает только при наличии таких инструкций в карточке И при включенной опции ''Приоритет инструкциям из карточек''.\nНЕ РЕКОМЕНДУЕТСЯ ДЛЯ МОДЕЛЕЙ TEXT COMPLETION, МОЖЕТ ПОРТИТЬ ВЫХОДНОЙ ТЕКСТ.", "First User Prefix": "Первый префикс пользователя", @@ -1914,8 +1914,8 @@ "Cannot restore GUI preset": "Пресет для Gui восстановить нельзя", "Default preset cannot be restored": "Невозможно восстановить пресет по умолчанию", "Default template cannot be restored": "Невозможно восстановить шаблон по умолчанию", - "Resetting a default preset will restore the default settings": "Сброс стандартного пресета восстановит настройки по умолчанию.", - "Resetting a default template will restore the default settings.": "Сброс стандартного шаблона восстановит настройки по умолчанию.", + "Resetting a default preset will restore the default settings.": "Сброс комплектного пресета восстановит настройки по умолчанию.", + "Resetting a default template will restore the default settings.": "Сброс комплектного шаблона восстановит настройки по умолчанию.", "Are you sure?": "Вы уверены?", "Default preset restored": "Стандартный пресет восстановлен", "Default template restored": "Стандартный шаблон восстановлен", @@ -2046,11 +2046,11 @@ "prompt_post_processing_merge": "Объединять идущие подряд сообщения с одной ролью", "prompt_post_processing_semi": "Semi-strict (чередовать роли)", "prompt_post_processing_strict": "Strict (чередовать роли, сначала пользователь)", - "Select Horde models": "Выбрать модель из Horde", + "Select Horde models": "Выберите модель из Horde", "Model ID (optional)": "Идентификатор модели (необязательно)", "Derive context size from backend": "Использовать бэкенд для определения размера контекста", "Rename current preset": "Переименовать пресет", - "No Worlds active. Click here to select.": "Нет активных миров. Нажмите, чтобы выбрать.", + "No Worlds active. Click here to select.": "Активных миров нет, ЛКМ для выбора.", "Title/Memo": "Название", "Strategy": "Статус", "Position": "Позиция", @@ -2169,7 +2169,7 @@ "instruct_derived": "Считывать из метаданных модели (по возможности)", "Confirm token parsing with": "Чтобы убедиться в правильности выделения токенов, используйте", "Reasoning Effort": "Рассуждения", - "Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей которые поддерживают эту возможность.\nНа данный момент поддерживаются три значения: Подробные, Обычные, Поверхностные.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.", + "Constrains effort on reasoning for reasoning models.": "Регулирует объём внутренних рассуждений модели (reasoning), для моделей, которые поддерживают эту возможность.\nПри менее подробном рассуждении ответ получается быстрее, а также экономятся токены, уходящие на рассуждения.", "openai_reasoning_effort_low": "Поверхностные", "openai_reasoning_effort_medium": "Обычные", "openai_reasoning_effort_high": "Подробные", @@ -2274,8 +2274,8 @@ "Persona Name Not Set": "У персоны отсутствует имя", "You must bind a name to this persona before you can set a lorebook.": "Перед привязкой лорбука персоне необходимо присвоить имя.", "Default Persona Removed": "Персона по умолчанию снята", - "Persona is locked to the current character": "Персона закреплена за этим персонажем", - "Persona is locked to the current chat": "Персона закреплена за этим чатом", + "Persona is locked to the current character": "Персона закреплена за текущим персонажем", + "Persona is locked to the current chat": "Персона закреплена за текущим чатом", "characters": "перс.", "character": "персонаж", "in this group": "в группе", @@ -2336,5 +2336,88 @@ "Reasoning already exists.": "Рассуждения уже присутствуют.", "Edit Message": "Редактирование", "Status check bypassed": "Проверка статуса отключена", - "Valid": "Работает" + "Valid": "Работает", + "Use Group Scoring": "Использовать Group Scoring", + "Only the entries with the most number of key matches will be selected for Inclusion Group filtering": "До групповых фильтров будут допущены только записи с наибольшим кол-вом совпадений", + "Can be used to automatically activate Quick Replies": "Используется для автоматической активации быстрых ответов (Quick Replies)", + "( None )": "(Отсутствует)", + "Tie this entry to specific characters or characters with specific tags": "Привязать запись к опред. персонажам или персонажам с заданными тегами", + "Move Entry to Another Lorebook": "Переместить запись в другой лорбук", + "There are no other lorebooks to move to.": "Некуда перемещать: не найдено других лорбуков.", + "Select Target Lorebook": "Выберите куда переместить", + "Move '${0}' to:": "Переместить '${0}' в:", + "Please select a target lorebook.": "Выберите лорбук, в который будет перемещена запись.", + "Scan depth cannot be negative": "Глубина сканирования не может быть отрицательной", + "Scan depth cannot exceed ${0}": "Глубина сканирования не может превышать ${0}", + "Select your current Reasoning Template": "Выберите текущий Шаблон рассуждений", + "Delete template": "Удалить шаблон", + "Reasoning Template": "Шаблон рассуждений", + "openai_reasoning_effort_auto": "Авто", + "openai_reasoning_effort_minimum": "Минимальные", + "openai_reasoning_effort_maximum": "Максимальные", + "OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.": "OpenAI принимает следующее: low (Поверхностные), medium (Обычные), high (Подробные). Minimum (Минимальные) - то же самое, что low. Maximum (Максимальные) - то же самое, что high. При выборе Auto (Авто) значение не отсылается вообще.", + "Allocates a portion of the response length for thinking (low: 10%, medium: 25%, high: 50%). Other options are model-dependent.": "Резервирует часть ответа для рассуждений (Поверхностные: 10% ответа, Обычные: 25%, Подробные: 50%). Остальные значения зависят от конкретной модели.", + "xAI Model": "Модель xAI", + "xAI API Key": "Ключ от API xAI", + "HuggingFace Token": "Токен HuggingFace", + "Endpoint URL": "Адрес эндпоинта", + "Example: https://****.endpoints.huggingface.cloud": "Пример: https://****.endpoints.huggingface.cloud", + "Featherless Model Selection": "Выбор модели из Featherless", + "category": "категория", + "Top": "Топовые", + "All Classes": "Все классы", + "Date Asc": "Дата, возрастание", + "Date Desc": "Дата, убывание", + "Background Image": "Фоновое изображение", + "Delete the background?": "Удалить фон?", + "Tags_as_Folders_desc": "Чтобы тег отображался как папка, его нужно отметить таковым в меню управления тегами. Нажмите сюда, чтобы открыть его.", + "tag_entries": "раз исп.", + "Multiple personas are connected to this character.\nSelect a persona to use for this chat.": "К этому персонажу привязано несколько персон.\nВыберите персону, которую хотите использовать в этом чате.", + "Select Persona": "Выберите персону", + "Completion Object": "Как часть Completion Object", + "Move ${0} to:": "Переместить '${0}' в:", + "Chat Scenario Override": "Перезапись сценария чата", + "Unique to this chat.": "Действует только в рамках текущего чата.", + "All group members will use the following scenario text instead of what is specified in their character cards.": "Все участники группы будут использовать этот сценарий вместо того, который указан в карточке.", + "Checkpoints inherit the scenario override from their parent, and can be changed individually after that.": "Чекпоинты наследуют сценарий родителя, после отделения его можно менять.", + "Delete Tag": "Удалить тег", + "Do you want to delete the tag": "Вы точно хотите удалить тег", + "If you want to merge all references to this tag into another tag, select it below:": "Если хотите заменить ссылки на этот тег на какой-то другой, то выберите из списка:", + "Open Folder (Show all characters even if not selected)": "Открытая папка (показать всех персонажей, включая невыбранных)", + "Closed Folder (Hide all characters unless selected)": "Закрытая папка (скрыть всех персонажей, кроме выбранных)", + "No Folder": "Не папка", + "Show only favorites": "Показать только избранных персонажей", + "Show only groups": "Показать только группы", + "Show only folders": "Показать только папки", + "Manage tags": "Панель управления тегами", + "Show Tag List": "Показать список тегов", + "Clear all filters": "Сбросить все фильтры", + "There are no items to display.": "Отображать абсолютно нечего.", + "Characters and groups hidden by filters or closed folders": "Персонажи и группы скрыты настройками фильтров либо закрытыми папками", + "Otterly empty": "Всё что можно, всё выдрано", + "Here be dragons": "Список настолько очистился, что в него вернулись драконы", + "Kiwibunga": "Настолько пусто, что киви прилетела посидеть", + "Pump-a-Rum": "Пу-пу-пу", + "Croak it": "Только кваканье лягушек и стрёкот сверчков", + "${0} character hidden.": "Персонажей скрыто: ${0}.", + "${0} characters hidden.": "Персонажей скрыто: ${0}.", + "/ page": "/ стр.", + "Context Length": "Размер контекста", + "Added On": "Добавлена", + "Class": "Класс", + "Bulk_edit_characters": "Массовое редактирование персонажей\n\nЛКМ, чтобы выделить либо отменить выделение персонажа\nShift+ЛКМ, чтобы массово выделить либо отменить выделение персонажей\nПКМ, чтобы выбрать действие", + "Bulk select all characters": "Выбрать всех персонажей", + "Duplicate": "Клонировать", + "Next page": "След. страница", + "Previous page": "Пред. страница", + "Group: ${0}": "Группа: ${0}", + "You deleted a character/chat and arrived back here for safety reasons! Pick another character!": "Вы удалили персонажа или чат, и мы из соображений безопасности перенесли вас на эту страницу! Выберите другого персонажа!", + "Group is empty.": "Группа пуста.", + "No characters available": "Персонажей нет", + "Choose what to export": "Выберите, что экспортировать", + "Text Completion Preset": "Пресет для режима Text Completion", + "Update enabled": "Обновить включенные", + "Could not connect to API": "Не удалось подключиться к API", + "Connected to API": "Соединение с API установлено", + "Go back": "Назад" } diff --git a/public/script.js b/public/script.js index 7d851e87f..cad2f20ed 100644 --- a/public/script.js +++ b/public/script.js @@ -143,6 +143,7 @@ import { getHordeModels, adjustHordeGenerationParams, MIN_LENGTH, + initHorde, } from './scripts/horde.js'; import { @@ -175,6 +176,9 @@ import { saveBase64AsFile, uuidv4, equalsIgnoreCaseAndAccents, + localizePagination, + renderPaginationDropdown, + paginationDropdownChangeHandler, } from './scripts/utils.js'; import { debounce_timeout, IGNORE_SYMBOL } from './scripts/constants.js'; @@ -994,6 +998,7 @@ async function firstLoadInit() { initAuthorsNote(); await initPersonas(); initWorldInfo(); + initHorde(); initRossMods(); initStats(); initCfg(); @@ -1423,30 +1428,26 @@ function getBackBlock() { return template; } -function getEmptyBlock() { +async function getEmptyBlock() { const icons = ['fa-dragon', 'fa-otter', 'fa-kiwi-bird', 'fa-crow', 'fa-frog']; - const texts = ['Here be dragons', 'Otterly empty', 'Kiwibunga', 'Pump-a-Rum', 'Croak it']; + const texts = [t`Here be dragons`, t`Otterly empty`, t`Kiwibunga`, t`Pump-a-Rum`, t`Croak it`]; const roll = new Date().getMinutes() % icons.length; - const emptyBlock = ` -
- -

${texts[roll]}

-

There are no items to display.

-
`; + const params = { + text: texts[roll], + icon: icons[roll], + }; + const emptyBlock = await renderTemplateAsync('emptyBlock', params); return $(emptyBlock); } /** * @param {number} hidden Number of hidden characters */ -function getHiddenBlock(hidden) { - const hiddenBlock = ` -
- -

${hidden} ${hidden > 1 ? 'characters' : 'character'} hidden.

-
-
-
`; +async function getHiddenBlock(hidden) { + const params = { + text: (hidden > 1 ? t`${hidden} characters hidden.` : t`${hidden} character hidden.`), + }; + const hiddenBlock = await renderTemplateAsync('hiddenBlock', params); return $(hiddenBlock); } @@ -1526,10 +1527,11 @@ export async function printCharacters(fullRefresh = false) { const entities = getEntitiesList({ doFilter: true }); + const pageSize = Number(accountStorage.getItem(storageKey)) || per_page_default; + const sizeChangerOptions = [10, 25, 50, 100, 250, 500, 1000]; $('#rm_print_characters_pagination').pagination({ dataSource: entities, - pageSize: Number(accountStorage.getItem(storageKey)) || per_page_default, - sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000], + pageSize, pageRange: 1, pageNumber: saveCharactersPage || 1, position: 'top', @@ -1538,14 +1540,16 @@ export async function printCharacters(fullRefresh = false) { prevText: '<', nextText: '>', formatNavigator: PAGINATION_TEMPLATE, + formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions), showNavigator: true, - callback: function (/** @type {Entity[]} */ data) { + callback: async function (/** @type {Entity[]} */ data) { $(listId).empty(); if (power_user.bogus_folders && isBogusFolderOpen()) { $(listId).append(getBackBlock()); } if (!data.length) { - $(listId).append(getEmptyBlock()); + const emptyBlock = await getEmptyBlock(); + $(listId).append(emptyBlock); } let displayCount = 0; for (const i of data) { @@ -1566,13 +1570,16 @@ export async function printCharacters(fullRefresh = false) { const hidden = (characters.length + groups.length) - displayCount; if (hidden > 0 && entitiesFilter.hasAnyFilter()) { - $(listId).append(getHiddenBlock(hidden)); + const hiddenBlock = await getHiddenBlock(hidden); + $(listId).append(hiddenBlock); } + localizePagination($('#rm_print_characters_pagination')); eventSource.emit(event_types.CHARACTER_PAGE_LOADED); }, - afterSizeSelectorChange: function (e) { + afterSizeSelectorChange: function (e, size) { accountStorage.setItem(storageKey, e.target.value); + paginationDropdownChangeHandler(e, size); }, afterPaging: function (e) { saveCharactersPage = e; @@ -8430,15 +8437,15 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, w function getOkButtonText() { if (['text', 'char_not_selected'].includes(popup_type)) { $dialoguePopupCancel.css('display', 'none'); - return okButton ?? 'Ok'; + return okButton ?? t`Ok`; } else if (['delete_extension'].includes(popup_type)) { - return okButton ?? 'Ok'; + return okButton ?? t`Ok`; } else if (['new_chat', 'confirm'].includes(popup_type)) { - return okButton ?? 'Yes'; + return okButton ?? t`Yes`; } else if (['input'].includes(popup_type)) { return okButton ?? t`Save`; } - return okButton ?? 'Delete'; + return okButton ?? t`Delete`; } dialogueCloseStop = true; diff --git a/public/scripts/backgrounds.js b/public/scripts/backgrounds.js index 308aac532..64ffbeabd 100644 --- a/public/scripts/backgrounds.js +++ b/public/scripts/backgrounds.js @@ -6,6 +6,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { flashHighlight, stringFormat } from './utils.js'; import { t } from './i18n.js'; +import { Popup } from './popup.js'; const BG_METADATA_KEY = 'custom_background'; const LIST_METADATA_KEY = 'chat_backgrounds'; @@ -291,7 +292,7 @@ async function onDeleteBackgroundClick(e) { const bgToDelete = $(this).closest('.bg_example'); const url = bgToDelete.data('url'); const isCustom = bgToDelete.attr('custom') === 'true'; - const confirm = await callPopup('

Delete the background?

', 'confirm'); + const confirm = await Popup.show.confirm(t`Delete the background?`, null); const bg = bgToDelete.attr('bgfile'); if (confirm) { diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 91cf0a88d..b3b0f3806 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -13,6 +13,9 @@ import { getBase64Async, resetScrollHeight, initScrollHeight, + localizePagination, + renderPaginationDropdown, + paginationDropdownChangeHandler, } from './utils.js'; import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js'; import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js'; @@ -1374,6 +1377,8 @@ function getGroupCharacters({ doFilter, onlyMembers } = {}) { function printGroupCandidates() { const storageKey = 'GroupCandidates_PerPage'; + const pageSize = Number(accountStorage.getItem(storageKey)) || 5; + const sizeChangerOptions = [5, 10, 25, 50, 100, 200, 500, 1000]; $('#rm_group_add_members_pagination').pagination({ dataSource: getGroupCharacters({ doFilter: true, onlyMembers: false }), pageRange: 1, @@ -1382,18 +1387,20 @@ function printGroupCandidates() { prevText: '<', nextText: '>', formatNavigator: PAGINATION_TEMPLATE, + formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions), showNavigator: true, showSizeChanger: true, - pageSize: Number(accountStorage.getItem(storageKey)) || 5, - sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000], - afterSizeSelectorChange: function (e) { + pageSize, + afterSizeSelectorChange: function (e, size) { accountStorage.setItem(storageKey, e.target.value); + paginationDropdownChangeHandler(e, size); }, callback: function (data) { $('#rm_group_add_members').empty(); for (const i of data) { $('#rm_group_add_members').append(getGroupCharacterBlock(i.item)); } + localizePagination($('#rm_group_add_members_pagination')); }, }); } @@ -1401,6 +1408,9 @@ function printGroupCandidates() { function printGroupMembers() { const storageKey = 'GroupMembers_PerPage'; $('.rm_group_members_pagination').each(function () { + let that = this; + const pageSize = Number(accountStorage.getItem(storageKey)) || 5; + const sizeChangerOptions = [5, 10, 25, 50, 100, 200, 500, 1000]; $(this).pagination({ dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }), pageRange: 1, @@ -1411,16 +1421,18 @@ function printGroupMembers() { formatNavigator: PAGINATION_TEMPLATE, showNavigator: true, showSizeChanger: true, - pageSize: Number(accountStorage.getItem(storageKey)) || 5, - sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000], - afterSizeSelectorChange: function (e) { + formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions), + pageSize, + afterSizeSelectorChange: function (e, size) { accountStorage.setItem(storageKey, e.target.value); + paginationDropdownChangeHandler(e, size); }, callback: function (data) { $('.rm_group_members').empty(); for (const i of data) { $('.rm_group_members').append(getGroupCharacterBlock(i.item)); } + localizePagination($(that)); }, }); }); @@ -1804,7 +1816,7 @@ async function createGroup() { const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(', '); if (!name) { - name = `Group: ${memberNames}`; + name = t`Group: ${memberNames}`; } const avatar_url = $('#group_avatar_preview img').attr('src'); diff --git a/public/scripts/horde.js b/public/scripts/horde.js index fc5ca93f2..1cc5694a7 100644 --- a/public/scripts/horde.js +++ b/public/scripts/horde.js @@ -394,7 +394,7 @@ function getHordeModelTemplate(option) { `)); } -jQuery(function () { +export function initHorde () { $('#horde_model').on('mousedown change', async function (e) { console.log('Horde model change', e); horde_settings.models = $('#horde_model').val(); @@ -441,7 +441,7 @@ jQuery(function () { if (!isMobile()) { $('#horde_model').select2({ width: '100%', - placeholder: 'Select Horde models', + placeholder: t`Select Horde models`, allowClear: true, closeOnSelect: false, templateSelection: function (data) { @@ -451,5 +451,5 @@ jQuery(function () { templateResult: getHordeModelTemplate, }); } -}); +} diff --git a/public/scripts/openai.js b/public/scripts/openai.js index d1b996c37..b37560fbb 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -3851,7 +3851,7 @@ function createLogitBiasListItem(entry) { } async function createNewLogitBiasPreset() { - const name = await callPopup('Preset name:', 'input'); + const name = await Popup.show.input(t`Preset name:`, null); if (!name) { return; diff --git a/public/scripts/personas.js b/public/scripts/personas.js index b302c37a0..1aeefa0b9 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -22,7 +22,7 @@ import { } from '../script.js'; import { persona_description_positions, power_user } from './power-user.js'; import { getTokenCountAsync } from './tokenizers.js'; -import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js'; +import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler } from './utils.js'; import { debounce_timeout } from './constants.js'; import { FILTER_TYPES, FilterHelper } from './filters.js'; import { groups, selected_group } from './group-chats.js'; @@ -250,16 +250,18 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { const storageKey = 'Personas_PerPage'; const listId = '#user_avatar_block'; const perPage = Number(accountStorage.getItem(storageKey)) || 5; + const sizeChangerOptions = [5, 10, 25, 50, 100, 250, 500, 1000]; $('#persona_pagination_container').pagination({ dataSource: entities, pageSize: perPage, - sizeChangerOptions: [5, 10, 25, 50, 100, 250, 500, 1000], + sizeChangerOptions, pageRange: 1, pageNumber: savePersonasPage || 1, position: 'top', showPageNumbers: false, showSizeChanger: true, + formatSizeChanger: renderPaginationDropdown(perPage, sizeChangerOptions), prevText: '<', nextText: '>', formatNavigator: PAGINATION_TEMPLATE, @@ -270,9 +272,11 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { $(listId).append(getUserAvatarBlock(item)); } updatePersonaUIStates(); + localizePagination($('#persona_pagination_container')); }, - afterSizeSelectorChange: function (e) { + afterSizeSelectorChange: function (e, size) { accountStorage.setItem(storageKey, e.target.value); + paginationDropdownChangeHandler(e, size); }, afterPaging: function (e) { savePersonasPage = e; diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 6f7f98298..2705da21c 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -27,7 +27,7 @@ import { debounce_timeout } from './constants.js'; import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js'; import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { renderTemplateAsync } from './templates.js'; -import { t } from './i18n.js'; +import { t, translate } from './i18n.js'; export { TAG_FOLDER_TYPES, @@ -318,7 +318,7 @@ function getTagBlock(tag, entities, hidden = 0, isUseless = false) { template.find('.avatar').css({ 'background-color': tag.color, 'color': tag.color2 }).attr('title', `[Folder] ${tag.name}`); template.find('.ch_name').text(tag.name).attr('title', `[Folder] ${tag.name}`); template.find('.bogus_folder_hidden_counter').text(hidden > 0 ? `${hidden} hidden` : ''); - template.find('.bogus_folder_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`); + template.find('.bogus_folder_counter').text(`${count} ` + (count != 1 ? t`characters` : t`character`)); template.find('.bogus_folder_icon').addClass(tagFolder.fa_icon); if (isUseless) template.addClass('useless'); @@ -1057,7 +1057,7 @@ function appendTagToList(listElement, tag, { removable = false, isFilter = false tagElement.attr('title', tag.title); } if (tag.icon) { - tagElement.find('.tag_name').text('').attr('title', `${tag.name} ${tag.title || ''}`.trim()).addClass(tag.icon); + tagElement.find('.tag_name').text('').attr('title', `${translate(tag.name)} ${tag.title || ''}`.trim()).addClass(tag.icon); tagElement.addClass('actionable'); } @@ -1644,6 +1644,7 @@ function updateDrawTagFolder(element, tag) { // Draw/update css attributes for this class folderElement.attr('title', tagFolder.tooltip); + folderElement.attr('data-i18n', '[title]' + tagFolder.tooltip); const indicator = folderElement.find('.tag_folder_indicator'); indicator.text(tagFolder.icon); indicator.css('color', tagFolder.color); @@ -1655,14 +1656,7 @@ async function onTagDeleteClick() { const tag = tags.find(x => x.id === id); const otherTags = sortTags(tags.filter(x => x.id !== id).map(x => ({ id: x.id, name: x.name }))); - const popupContent = $(` -

Delete Tag

-
Do you want to delete the tag
?
-
If you want to merge all references to this tag into another tag, select it below:
- `); + const popupContent = $(await renderTemplateAsync('deleteTag', { otherTags })); appendTagToList(popupContent.find('#tag_to_delete'), tag); diff --git a/public/scripts/templates/deleteTag.html b/public/scripts/templates/deleteTag.html new file mode 100644 index 000000000..667ccc00f --- /dev/null +++ b/public/scripts/templates/deleteTag.html @@ -0,0 +1,9 @@ +

Delete Tag

+
Do you want to delete the tag
?
+
If you want to merge all references to this tag into another tag, select it below:
+ \ No newline at end of file diff --git a/public/scripts/templates/emptyBlock.html b/public/scripts/templates/emptyBlock.html new file mode 100644 index 000000000..3ff057acd --- /dev/null +++ b/public/scripts/templates/emptyBlock.html @@ -0,0 +1,7 @@ +
+ +

{{text}}

+

+ There are no items to display. +

+
diff --git a/public/scripts/templates/hiddenBlock.html b/public/scripts/templates/hiddenBlock.html new file mode 100644 index 000000000..6b1d84406 --- /dev/null +++ b/public/scripts/templates/hiddenBlock.html @@ -0,0 +1,6 @@ +
+ +

{{text}}

+
+
+
diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index 51f153f23..a607d87b9 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -7,6 +7,7 @@ import { renderTemplateAsync } from './templates.js'; import { POPUP_TYPE, callGenericPopup } from './popup.js'; import { t } from './i18n.js'; import { accountStorage } from './util/AccountStorage.js'; +import { localizePagination, PAGINATION_TEMPLATE } from './utils.js'; let mancerModels = []; let togetherModels = []; @@ -361,9 +362,7 @@ export async function loadFeatherlessModels(data) { showSizeChanger: false, prevText: '<', nextText: '>', - formatNavigator: function (currentPage, totalPage) { - return (currentPage - 1) * perPage + 1 + ' - ' + currentPage * perPage + ' of ' + totalPage * perPage; - }, + formatNavigator: PAGINATION_TEMPLATE, showNavigator: true, callback: function (modelsOnPage, pagination) { modelCardBlock.innerHTML = ''; @@ -385,15 +384,15 @@ export async function loadFeatherlessModels(data) { const modelClassDiv = document.createElement('div'); modelClassDiv.classList.add('model-class'); - modelClassDiv.textContent = `Class: ${model.model_class || 'N/A'}`; + modelClassDiv.textContent = t`Class` + `: ${model.model_class || 'N/A'}`; const contextLengthDiv = document.createElement('div'); contextLengthDiv.classList.add('model-context-length'); - contextLengthDiv.textContent = `Context Length: ${model.context_length}`; + contextLengthDiv.textContent = t`Context Length` + `: ${model.context_length}`; const dateAddedDiv = document.createElement('div'); dateAddedDiv.classList.add('model-date-added'); - dateAddedDiv.textContent = `Added On: ${new Date(model.created * 1000).toLocaleDateString()}`; + dateAddedDiv.textContent = t`Added On` + `: ${new Date(model.created * 1000).toLocaleDateString()}`; detailsContainer.appendChild(modelClassDiv); detailsContainer.appendChild(contextLengthDiv); @@ -417,6 +416,7 @@ export async function loadFeatherlessModels(data) { // Update the current page value whenever the page changes featherlessCurrentPage = pagination.pageNumber; + localizePagination(paginationContainer); }, afterSizeSelectorChange: function (e) { const newPerPage = e.target.value; diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 56b4f4a70..6f13a9794 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -20,7 +20,46 @@ import { getCurrentLocale, t } from './i18n.js'; * Pagination status string template. * @type {string} */ -export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of <%= totalNumber %>'; +export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> .. <%= totalNumber %>'; + +export const localizePagination = function(container) { + container.find('[title="Next page"]').attr('title', t`Next page`); + container.find('[title="Previous page"]').attr('title', t`Previous page`); +}; + +/** + * Renders a dropdown for selecting page size in pagination. + * @param {number} pageSize Page size + * @param {number[]} sizeChangerOptions Array of page size options + * @returns {string} The rendered dropdown element as a string + */ +export const renderPaginationDropdown = function(pageSize, sizeChangerOptions) { + const sizeSelect = document.createElement('select'); + sizeSelect.classList.add('J-paginationjs-size-select'); + + if (sizeChangerOptions.indexOf(pageSize) === -1) { + sizeChangerOptions.unshift(pageSize); + sizeChangerOptions.sort((a, b) => a - b); + } + + for (let i = 0; i < sizeChangerOptions.length; i++) { + const option = document.createElement('option'); + option.value = `${sizeChangerOptions[i]}`; + option.textContent = `${sizeChangerOptions[i]} ${t`/ page`}`; + if (sizeChangerOptions[i] === pageSize) { + option.setAttribute('selected', 'selected'); + } + sizeSelect.appendChild(option); + } + + return sizeSelect.outerHTML; +}; + +export const paginationDropdownChangeHandler = function(event, size) { + let dropdown = $(event?.originalEvent?.currentTarget || event.delegateTarget).find('select'); + dropdown.find('[selected]').removeAttr('selected'); + dropdown.find(`[value=${size}]`).attr('selected', ''); +}; /** * Navigation options for pagination. diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 99393ab9b..b6b2f01fd 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -2658,7 +2658,7 @@ export async function getWorldEntry(name, data, entry) { if (!isMobile()) { $(characterFilter).select2({ width: '100%', - placeholder: 'Tie this entry to specific characters or characters with specific tags', + placeholder: t`Tie this entry to specific characters or characters with specific tags`, allowClear: true, closeOnSelect: false, }); @@ -3258,7 +3258,7 @@ export async function getWorldEntry(name, data, entry) { // Create wrapper div const wrapper = document.createElement('div'); - wrapper.textContent = t`Move "${sourceName}" to:`; + wrapper.textContent = t`Move '${sourceName}' to:`; // Create container and append elements const container = document.createElement('div'); diff --git a/public/style.css b/public/style.css index faa7524bc..cc29f1551 100644 --- a/public/style.css +++ b/public/style.css @@ -5715,6 +5715,7 @@ body:not(.movingUI) .drawer-content.maximized { width: unset; margin: 0; font-size: calc(var(--mainFontSize) * 0.85); + padding-right: 20px; } .paginationjs-pages ul li a {