add bot strings to i18n and fix dilogue width

This commit is contained in:
pdxlocations
2026-03-28 22:22:41 -07:00
parent 8ee21b1973
commit 6721874937
7 changed files with 134 additions and 12 deletions
+6
View File
@@ -78,6 +78,7 @@ help.traceroute, "Ctrl+T or F4 = Traceroute", ""
help.node_info, "F5 = Full node info", ""
help.archive_chat, "Ctrl+D = Archive chat / remove node", ""
help.favorite, "Ctrl+F = Favorite", ""
help.bot_responder, "Ctrl+B = Toggle Bot Responder", ""
help.ignore, "Ctrl+G = Ignore", ""
help.search, "Ctrl+/ or / = Search", ""
help.help, "Ctrl+K = Help", ""
@@ -90,6 +91,11 @@ confirm.remove_ignored, "Remove {name} from Ignored?", ""
confirm.region_unset, "Your region is UNSET. Set it now?", ""
dialog.resize_title, "Resize Terminal", ""
dialog.resize_body, "Please resize the terminal to at least {rows} rows.", ""
bot.status.enabled, "Enabled", ""
bot.status.disabled, "Disabled", ""
bot.dialog.title, "Bot Responder", ""
bot.dialog.body, "Bot responder is now {status}.", ""
bot.status.message, "Bot responder is now {status}.", ""
bot.catch_words, "ping; test", "Semicolon-separated bot trigger words."
bot.response.word, "Pong!", "Bot response word."
+6
View File
@@ -78,10 +78,16 @@ help.traceroute, "Ctrl+T ou F4 = Traceroute", ""
help.node_info, "F5 = Informations complètes du nœud", ""
help.archive_chat, "Ctrl+D = Archiver la discussion / supprimer le nœud", ""
help.favorite, "Ctrl+F = Favori", ""
help.bot_responder, "Ctrl+B = Activer/désactiver le bot répondeur", ""
help.ignore, "Ctrl+G = Ignorer", ""
help.search, "Ctrl+/ ou / = Rechercher", ""
help.help, "Ctrl+K = Aide", ""
help.no_help, "Aucune aide disponible.", ""
bot.status.enabled, "Activé", ""
bot.status.disabled, "Désactivé", ""
bot.dialog.title, "Bot répondeur", ""
bot.dialog.body, "Le bot répondeur est maintenant {status}.", ""
bot.status.message, "Le bot répondeur est maintenant {status}.", ""
bot.catch_words, "ping", "Mots déclencheurs du bot séparés par des virgules."
bot.response.word, "Pong!", "Mot de reponse du bot (orthographe preferee)."
+6
View File
@@ -78,6 +78,7 @@ help.traceroute, "Ctrl+T или F4 = Traceroute", ""
help.node_info, "F5 = Полная информация об узле", ""
help.archive_chat, "Ctrl+D = Архив чата / удалить узел", ""
help.favorite, "Ctrl+F = Избранное", ""
help.bot_responder, "Ctrl+B = Вкл/выкл автоответчик", ""
help.ignore, "Ctrl+G = Игнорировать", ""
help.search, "Ctrl+/ или / = Поиск", ""
help.help, "Ctrl+K = Справка", ""
@@ -90,6 +91,11 @@ confirm.remove_ignored, "Убрать {name} из игнорируемых?", ""
confirm.region_unset, "Ваш регион НЕ ЗАДАН. Установить сейчас?", ""
dialog.resize_title, "Увеличьте окно", ""
dialog.resize_body, "Пожалуйста, увеличьте окно до {rows} строк.", ""
bot.status.enabled, "Включен", ""
bot.status.disabled, "Выключен", ""
bot.dialog.title, "Автоответчик", ""
bot.dialog.body, "Автоответчик теперь {status}.", ""
bot.status.message, "Автоответчик теперь {status}.", ""
bot.catch_words, "ping; пинг", "Слова для активации бота, разделенные точкой с запятой."
bot.response.word, "Понг!", "Ответное слово бота (предпочтительное написание)."
+5 -5
View File
@@ -937,14 +937,14 @@ def handle_ctrl_k(stdscr: curses.window) -> None:
def handle_ctrl_b(stdscr: curses.window) -> None:
"""Handle Ctrl + B key events to toggle automatic bot responses."""
ui_state.bot_mode_enabled = not ui_state.bot_mode_enabled
status = t("ui.status.enabled", default="Enabled") if ui_state.bot_mode_enabled else t(
"ui.status.disabled", default="Disabled"
status = t("ui.bot.status.enabled", default="Enabled") if ui_state.bot_mode_enabled else t(
"ui.bot.status.disabled", default="Disabled"
)
curses.curs_set(0)
contact.ui.dialog.dialog(
t("ui.dialog.bot_responder_title", default="Bot Responder"),
t("ui.dialog.bot_responder_body", default="Bot responder is now {status}.", status=status),
t("ui.bot.dialog.title", default="Bot Responder"),
t("ui.bot.dialog.body", default="Bot responder is now {status}.", status=status),
)
if ui_state.channel_list:
@@ -952,7 +952,7 @@ def handle_ctrl_b(stdscr: curses.window) -> None:
add_new_message(
channel_id,
f"{config.message_prefix} Info: ",
t("ui.status.bot_mode", default="Bot responder is now {status}.", status=status.lower()),
t("ui.bot.status.message", default="Bot responder is now {status}.", status=status.lower()),
)
draw_messages_window(True)
+6 -7
View File
@@ -2,7 +2,7 @@ import curses
from contact.utilities.i18n import t_text
from contact.ui.colors import get_color
from contact.ui.nav_utils import draw_main_arrows
from contact.ui.nav_utils import draw_main_arrows, slice_to_width, text_width
from contact.utilities.singleton import menu_state, ui_state
@@ -19,10 +19,10 @@ def dialog(title: str, message: str) -> None:
# Parse message into lines and calculate dimensions
message_lines = message.splitlines() or [""]
max_line_length = max(len(l) for l in message_lines)
max_line_length = max(text_width(l) for l in message_lines)
# Desired size
dialog_width = max(len(title) + 4, max_line_length + 4)
dialog_width = max(text_width(title) + 4, max_line_length + 4)
desired_height = len(message_lines) + 4
# Clamp dialog size to the screen (leave a 1-cell margin if possible)
@@ -61,7 +61,7 @@ def dialog(title: str, message: str) -> None:
# Title
try:
win.addstr(0, 2, title[: max(0, dialog_width - 4)], get_color("settings_default"))
win.addstr(0, 2, slice_to_width(title, max(0, dialog_width - 4)), get_color("settings_default"))
except curses.error:
pass
@@ -80,9 +80,8 @@ def dialog(title: str, message: str) -> None:
if idx >= len(message_lines):
break
line = message_lines[idx]
# Hard-trim lines that don't fit
trimmed = line[: max(0, dialog_width - 6)]
msg_x = max(0, ((dialog_width - 2) - len(trimmed)) // 2)
trimmed = slice_to_width(line, max(0, dialog_width - 4))
msg_x = max(0, ((dialog_width - 2) - text_width(trimmed)) // 2)
try:
msg_win.addstr(1 + i, msg_x, trimmed, get_color("settings_default"))
except curses.error:
+88
View File
@@ -0,0 +1,88 @@
import unittest
from unittest import mock
from contact.ui import dialog as dialog_module
from contact.utilities.singleton import menu_state, ui_state
class _FakeWindow:
def __init__(self, height: int, width: int) -> None:
self.height = height
self.width = width
self.children = []
self.added_strings = []
self._getch_values = [10]
def erase(self) -> None:
return None
def bkgd(self, *_args) -> None:
return None
def attrset(self, *_args) -> None:
return None
def border(self, *_args) -> None:
return None
def addstr(self, y: int, x: int, text: str, *_args) -> None:
self.added_strings.append((y, x, text))
def derwin(self, height: int, width: int, _y: int, _x: int):
child = _FakeWindow(height, width)
self.children.append(child)
return child
def noutrefresh(self) -> None:
return None
def keypad(self, *_args) -> None:
return None
def timeout(self, *_args) -> None:
return None
def getch(self) -> int:
return self._getch_values.pop(0) if self._getch_values else -1
def refresh(self) -> None:
return None
def getmaxyx(self):
return (self.height, self.width)
class DialogTests(unittest.TestCase):
def setUp(self) -> None:
self.previous_window = ui_state.current_window
self.previous_start_index = list(ui_state.start_index)
self.previous_need_redraw = menu_state.need_redraw
def tearDown(self) -> None:
ui_state.current_window = self.previous_window
ui_state.start_index = self.previous_start_index
menu_state.need_redraw = self.previous_need_redraw
def test_dialog_renders_full_message_when_width_is_sufficient(self) -> None:
root = _FakeWindow(5, 33)
ui_state.current_window = 0
ui_state.start_index = [0, 0, 0]
menu_state.need_redraw = False
with mock.patch.object(dialog_module, "t_text", side_effect=lambda text: text):
with mock.patch.object(dialog_module, "get_color", return_value=0):
with mock.patch.object(dialog_module, "draw_main_arrows"):
with mock.patch.object(dialog_module.curses, "update_lines_cols"):
with mock.patch.object(dialog_module.curses, "doupdate"):
with mock.patch.object(dialog_module.curses, "LINES", 24, create=True):
with mock.patch.object(dialog_module.curses, "COLS", 80, create=True):
with mock.patch.object(dialog_module.curses, "newwin", return_value=root):
dialog_module.dialog("Bot Responder", "Bot responder is now Enabled.")
self.assertTrue(root.children)
message_text = [text for _y, _x, text in root.children[0].added_strings]
self.assertIn("Bot responder is now Enabled.", message_text)
if __name__ == "__main__":
unittest.main()
+17
View File
@@ -5,6 +5,7 @@ from unittest import mock
import contact.ui.default_config as config
from contact.utilities import i18n
from contact.utilities.ini_utils import parse_ini_file
from tests.test_support import restore_config, snapshot_config
@@ -55,3 +56,19 @@ class I18nTests(unittest.TestCase):
config.language = "ru"
self.assertEqual(i18n.t("missing", default="fallback"), "fallback")
self.assertEqual(parse_ini_file.call_count, 2)
def test_bot_ui_translation_keys_exist_in_all_locales(self) -> None:
required_keys = {
"ui.help.bot_responder",
"ui.bot.status.enabled",
"ui.bot.status.disabled",
"ui.bot.dialog.title",
"ui.bot.dialog.body",
"ui.bot.status.message",
"ui.bot.catch_words",
"ui.bot.response.word",
}
for language in config.get_localisation_options():
field_mapping, _ = parse_ini_file(config.get_localisation_file(language))
self.assertTrue(required_keys.issubset(field_mapping), msg=f"Missing bot translation keys in {language}")