From 8d923d6a6a13a9f60a8aaa628359783d3d311dbb Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 8 Sep 2025 19:24:27 +0300 Subject: [PATCH] localize --- app/core/config.py | 2 +- app/core/localizer.py | 36 +++++++---- app/locales/en.json | 81 +++++------------------- app/locales/ru.json | 125 +++++-------------------------------- app/ui/views/login_view.py | 113 ++++++++++++++++++++++++--------- 5 files changed, 142 insertions(+), 215 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index db352c0..aceae10 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,4 +1,4 @@ -DEBUG = False +DEBUG = True BASE_URL = "https://api.yobble.org" APP_NAME = "yobble messenger" APP_VERSION = "0.1_login_screen_windows" diff --git a/app/core/localizer.py b/app/core/localizer.py index 97a758b..16a5110 100644 --- a/app/core/localizer.py +++ b/app/core/localizer.py @@ -1,19 +1,32 @@ import json import os from PySide6.QtCore import QSettings -import config +from app.core import config class Localizer: """Класс для загрузки и работы с переводами""" def __init__(self): - self.settings = QSettings("volna_desktop_app", "Localization") - self.lang = self.settings.value("language", "ru") # Загружаем сохраненный язык - #self.locales_path = os.path.join(os.path.dirname(__file__), "locales") + self.settings = QSettings("yobble_messenger", "Localization") self.locales_path = os.path.join("app/locales") self.translations = {} + + self.lang = self.settings.value("language", "ru") + available_langs = self.get_available_languages() + if self.lang not in available_langs: + self.lang = available_langs[0] if available_langs else "ru" + self.load_language(self.lang) + def get_available_languages(self): + """Возвращает список доступных языков по файлам в папке locales""" + langs = [] + if os.path.exists(self.locales_path): + for file in os.listdir(self.locales_path): + if file.endswith(".json"): + langs.append(os.path.splitext(file)[0]) + return langs + def load_language(self, lang): """Загружает перевод из JSON-файла""" lang_file = os.path.join(self.locales_path, f"{lang}.json") @@ -21,7 +34,7 @@ class Localizer: with open(lang_file, "r", encoding="utf-8") as file: self.translations = json.load(file) self.lang = lang - self.settings.setValue("language", lang) # Сохраняем выбор языка + self.settings.setValue("language", lang) if config.DEBUG: print(f"[Localizer.load_language] ✅ Язык загружен: {lang}") else: if config.DEBUG: print(f"[Localizer.load_language] ❌ Файл локализации не найден: {lang_file}") @@ -30,14 +43,15 @@ class Localizer: """Возвращает перевод слова""" text = self.translations.get(key, key) if code is not None: - text = f"{text} {code}" # Подставляем код ошибки в строку, если он есть + text = f"{text} {code}" return text def switch_language(self, lang): - """Переключает языки""" - if lang not in ["ru", "en"]: - lang = "ru" - self.load_language(lang) + """Переключает языки, если язык доступен""" + if lang in self.get_available_languages(): + self.load_language(lang) + else: + if config.DEBUG: print(f"[Localizer.switch_language] ❌ Язык не найден: {lang}") -# Создаем глобальный экземпляр, чтобы использовать в любом месте +# Глобальный экземпляр localizer = Localizer() diff --git a/app/locales/en.json b/app/locales/en.json index ad7e943..358657a 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -1,67 +1,18 @@ { - "title_error": "Error", - "auth_qLabel_text": "Volna", - "auth_title_login_windows": "Вход", - "auth_placeholder_text_login": "Email or number", - "auth_placeholder_text_password": "Password", - "auth_button_sign_in": "Sign in", - "auth_service_ping_error_else1": "⚠️ Server unavailable", - "auth_service_ping_error_exception1": "⚠️ Server unreachable", - "auth_service_login_error_else1": "🔴 Authentication error", - "auth_service_login_error_exception1": "🔴 Server connection failed", - "auth_service_user_not_employee_else1": "You are not an employee", - "auth_service_user_not_employee_exception1": "User validation error", - - "json_output_data_error": "❌ Error", - "json_output_data_details": "❌ Error description", - "json_output_data_diagnosis": "📋 Diagnosis", - "json_output_data_recommendations": "💡 Recommendations", - - "json_output_data_apri": "🎯 APRI", - "json_output_data_fib4": "🎯 FIB4", - "json_output_data_child_pugh_score": "🎯 Child-Pugh Score", - "json_output_data_child_pugh_class": "🎯 Child-Pugh Class", - - "json_input_data_code_name": "🔍 Analysis code", - "json_input_data_parameters": "📊 Parameters", - "json_input_data_AST": "🩸 AST (U/L)", - "json_input_data_ALT": "🩸 ALT (U/L)", - "json_input_data_Platelets": "🩸 Platelets (×10⁹/L)", - "json_input_data_Bilirubin": "🟡 Bilirubin (mg/dL)", - "json_input_data_Albumin": "⚪ Albumin (g/dL)", - "json_input_data_Age": "👶 Age", - "json_input_data_AST_ULN": "🔼 AST (Upper Limit)", - "json_input_data_Ascites": "💧 Ascites", - "json_input_data_Jaundice": "🟡 Jaundice", - "json_input_data_Encephalopathy": "🧠 Encephalopathy", - "json_input_data_LiverFibrosis": "🔄 Liver Fibrosis", - "json_input_data_GGT": "🟢 GGT (U/L)", - "json_input_data_ALP": "🔵 ALP (U/L)", - "json_input_data_INR": "🩸 INR", - "json_input_data_CRP": "🔴 CRP (mg/L)", - "json_input_data_Ferritin": "🟠 Ferritin (ng/mL)", - - "none": "❌ None", - "False": "❌ No", - "True": "✅ Yes", - - "test_status_-1": "❌ Error", - "test_status_0": "🆕 New", - "test_status_1": "⚙️ In Progress", - "test_status_2": "✅ Completed", - - "no cirrhosis": "No cirrhosis", - "possible cirrhosis": "Possible cirrhosis", - "high risk of cirrhosis": "High risk of cirrhosis", - - "monitor liver function": "Monitor liver function", - "consult hepatologist": "Consult a hepatologist", - "immediate medical attention required": "Immediate medical attention required", - "Possible cholestasis detected": "Possible cholestasis detected", - "Possible liver failure detected": "Possible liver failure detected", - "Possible inflammatory liver disease": "Possible inflammatory liver disease", - - "A (Compensated)": "Class A (Compensated Cirrhosis)", - "B (Decompensated)": "Class B (Decompensated Cirrhosis)", - "C (Severe Decompensated)": "Class C (Severe Decompensated Cirrhosis)" + "__name__": "English", + "Авторизация": "Login", + "Регистрация": "Register", + "Логин": "Username", + "Пароль": "Password", + "Повторите пароль": "Repeat password", + "Инвайт-код": "Invite code", + "Войти": "Login", + "Зарегистрироваться": "Sign up", + "Уже есть аккаунт? Войти": "Already have an account? Log in", + "Нет аккаунта? Регистрация": "Don't have an account? Sign up", + "Ошибка": "Error", + "Пожалуйста, исправьте ошибки в форме": "Please correct the form errors", + "Неверный логин или пароль": "Incorrect username or password", + "Пароли не совпадают": "Passwords do not match", + "Регистрация прошла успешно для": "Successfully registered for" } diff --git a/app/locales/ru.json b/app/locales/ru.json index 8e69d11..0c5a71d 100644 --- a/app/locales/ru.json +++ b/app/locales/ru.json @@ -1,111 +1,18 @@ { - "title_error": "Ошибка", - "auth_qLabel_text": "Volna", - "auth_title_login_windows": "Вход", - "auth_placeholder_text_login": "Почта или номер", - "auth_placeholder_text_password": "Пароль", - "auth_button_sign_in": "Войти", - "auth_service_ping_error_else1": "⚠️ Сервер недоступен", - "auth_service_ping_error_exception1": "⚠️ Сервер недоступен", - "auth_service_login_error_else1": "🔴 Ошибка авторизации", - "auth_service_login_error_exception1": "🔴 Ошибка подключения к серверу", - "auth_service_user_not_employee_else1": "Вы не являетесь сотрудником", - "auth_service_user_not_employee_exception1": "Ошибка проверки пользователя", - - "json_output_data_error": "❌ Ошибка", - "json_output_data_details": "❌ Описание ошибки", - "json_output_data_diagnosis": "📋 Диагноз", - "json_output_data_recommendations": "💡 Рекомендации", - - "json_output_data_apri": "🎯 APRI", - "json_output_data_fib4": "🎯 FIB4", - "json_output_data_child_pugh_score": "🎯 Чайлд-Пью Баллы", - "json_output_data_child_pugh_class": "🎯 Чайлд-Пью Класс", - - "json_input_data_code_name": "🔍 Код анализа", - "json_input_data_parameters": "📊 Параметры", - "json_input_data_AST": "🩸 AST (Ед/л)", - "json_input_data_ALT": "🩸 ALT (Ед/л)", - "json_input_data_Platelets": "🩸 Тромбоциты (×10⁹/л)", - "json_input_data_Bilirubin": "🟡 Билирубин (мг/дл)", - "json_input_data_Albumin": "⚪ Альбумин (г/дл)", - "json_input_data_Age": "👶 Возраст", - "json_input_data_AST_ULN": "🔼 AST (верхний предел)", - "json_input_data_Ascites": "💧 Асцит", - "json_input_data_Jaundice": "🟡 Желтуха", - "json_input_data_Encephalopathy": "🧠 Энцефалопатия", - "json_input_data_LiverFibrosis": "🔄 Фиброз печени", - "json_input_data_GGT": "🟢 GGT (Ед/л)", - "json_input_data_ALP": "🔵 ALP (Ед/л)", - "json_input_data_INR": "🩸 INR", - "json_input_data_CRP": "🔴 CRP (мг/л)", - "json_input_data_Ferritin": "🟠 Ферритин (нг/мл)", - - "AST": "AST (Ед/л)", - "ALT": "ALT (Ед/л)", - "Platelets": "Тромбоциты (×10⁹/л)", - "Bilirubin": "Билирубин (мг/дл)", - "Albumin": "Альбумин (г/дл)", - "Age": "Возраст", - "AST_ULN": "AST (верхний предел)", - "Ascites": "Асцит", - "Jaundice": "Желтуха", - "Encephalopathy": "Энцефалопатия", - "LiverFibrosis": "Фиброз печени", - "GGT": "GGT (Ед/л)", - "ALP": "ALP (Ед/л)", - "INR": "INR", - "CRP": "CRP (мг/л)", - "Ferritin": "Ферритин (нг/мл)", - - "json_input_data_True": "да", - "json_input_data_False": "нет", - "json_input_data_mild": "легкая", - "json_input_data_moderate": "умеренный", - "json_input_data_severe": "тяжелая", - - "json_output_data_dynamic_forecast": "📈 Аналитика", - "json_output_data_dynamic_forecast_slope": "Темпы изменения", - "json_output_data_dynamic_forecast_interpretation": "💬 Интерпретации", - - "json_output_data_slope": "📈 Прогнозирование рисков:", - "json_output_data_slope_apri": "📈 Изменения APRI", - "json_output_data_slope_fib4": "📈 Изменения FIB-4", - "json_output_data_slope_child_pugh": "📈 Изменения Чайлд-Пью", - "json_output_data_slope_childpugh": "📈 Изменения Чайлд-Пью", - "json_output_data_slope_ChildPugh": "📈 Изменения Чайлд-Пью", - - "advanced fibrosis or early cirrhosis": "Продвинутый фиброз или ранний цирроз", - "consider elastography or biopsy": "Рассмотреть эластографию или биопсию", - - "json_output_data_dynamic_forecast_slope_apri": "📈 Изменения APRI", - "json_output_data_dynamic_forecast_slope_APRI": "📈 Изменения APRI", - "json_output_data_dynamic_forecast_slope_fib4": "📈 Изменения FIB-4", - "json_output_data_dynamic_forecast_slope_FIB4": "📈 Изменения FIB-4", - "json_output_data_dynamic_forecast_slope_childpugh": "📈 Изменения Чайлд-Пью", - "json_output_data_dynamic_forecast_slope_ChildPugh": "📈 Изменения Чайлд-Пью", - - "none": "❌ Нет", - "False": "❌ Нет", - "True": "✅ Да", - - "test_status_-1": "❌ Ошибка", - "test_status_0": "🆕 Новый", - "test_status_1": "⚙️ В работе", - "test_status_2": "✅ Завершен", - - "no cirrhosis": "Нет цирроза", - "possible cirrhosis": "Возможный цирроз", - "high risk of cirrhosis": "Высокий риск цирроза", - - "monitor liver function": "Мониторинг функции печени", - "consult hepatologist": "Консультация с гепатологом", - "immediate medical attention required": "Требуется немедленная медицинская помощь", - "Possible cholestasis detected": "Возможен холестаз", - "Possible liver failure detected": "Возможна печёночная недостаточность", - "Possible inflammatory liver disease": "Возможное воспалительное заболевание печени", - - "A (Compensated)": "Класс A (Компенсированный цирроз)", - "B (Decompensated)": "Класс B (Декомпенсированный цирроз)", - "C (Severe Decompensated)": "Класс C (Тяжёлый декомпенсированный цирроз)" + "__name__": "Русский", + "Авторизация": "Авторизация", + "Регистрация": "Регистрация", + "Логин": "Логин", + "Пароль": "Пароль", + "Повторите пароль": "Повторите пароль", + "Инвайт-код": "Инвайт-код", + "Войти": "Войти", + "Зарегистрироваться": "Зарегистрироваться", + "Уже есть аккаунт? Войти": "Уже есть аккаунт? Войти", + "Нет аккаунта? Регистрация": "Нет аккаунта? Регистрация", + "Ошибка": "Ошибка", + "Пожалуйста, исправьте ошибки в форме": "Пожалуйста, исправьте ошибки в форме", + "Неверный логин или пароль": "Неверный логин или пароль", + "Пароли не совпадают": "Пароли не совпадают", + "Регистрация прошла успешно для": "Регистрация прошла успешно для" } diff --git a/app/ui/views/login_view.py b/app/ui/views/login_view.py index 9bfdf90..05b7253 100644 --- a/app/ui/views/login_view.py +++ b/app/ui/views/login_view.py @@ -1,6 +1,6 @@ from PySide6.QtWidgets import ( QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QMessageBox, - QHBoxLayout, QSpacerItem, QSizePolicy + QHBoxLayout, QSpacerItem, QSizePolicy, QComboBox ) from PySide6.QtCore import Qt from ..widgets.validation_input import ValidationInput @@ -8,6 +8,7 @@ from common_lib.utils.validators import ( validate_username as common_validate_username, validate_password as common_validate_password ) +from app.core.localizer import localizer def validate_username(username, is_login=False): if is_login: @@ -18,7 +19,19 @@ def validate_username(username, is_login=False): return common_validate_username(username, need_back=True) def validate_invite_code(invite_code): - return common_validate_username(invite_code, need_back=True) + # Login must be between 3 and 32 characters long + # Login must not contain whitespace characters + # Login must not start with an underscore + # Login must not contain consecutive underscores + # Login must contain only English letters, digits, and underscores + + # Invite must be between 3 and 32 characters long + # Invite must not contain whitespace characters + # Invite must not start with an underscore + # Invite must not contain consecutive underscores + # Invite must contain only English letters, digits, and underscores + + return common_validate_username(invite_code, field_name="invite", need_back=True) def validate_password(password, is_login=False): if is_login: @@ -45,19 +58,37 @@ class LoginView(QWidget): self.setFixedSize(400, 550) self.is_dark_theme = True + self.lang_combo = None self.is_registration = False self.init_ui() self.apply_dark_theme() def init_ui(self): - # Переключатель темы + # Переключатель темы и языка theme_layout = QHBoxLayout() theme_layout.setAlignment(Qt.AlignRight) self.theme_button = QPushButton("🌞") self.theme_button.setFixedWidth(50) self.theme_button.clicked.connect(self.toggle_theme) + + + self.lang_combo = QComboBox() + self.lang_combo.setFixedWidth(100) + self.lang_map = {} # lang_code: display_name + for lang in localizer.get_available_languages(): + # Попробуем достать из локализации display name (если есть) + lang_name = localizer.translations.get("__name__", lang) + self.lang_map[lang_name] = lang + self.lang_combo.addItem(lang_name) + # Устанавливаем текущий язык + current_lang_name = [k for k, v in self.lang_map.items() if v == localizer.lang] + if current_lang_name: + self.lang_combo.setCurrentText(current_lang_name[0]) + self.lang_combo.currentTextChanged.connect(self.change_language) + theme_layout.addSpacerItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + theme_layout.addWidget(self.lang_combo) theme_layout.addWidget(self.theme_button) # Основная часть @@ -65,7 +96,7 @@ class LoginView(QWidget): self.main_layout.setAlignment(Qt.AlignCenter) self.main_layout.setContentsMargins(40, 40, 40, 40) - self.title = QLabel("Авторизация") + self.title = QLabel(localizer.translate("Авторизация")) self.title.setAlignment(Qt.AlignCenter) self.title.setStyleSheet("font-size: 20px; font-weight: bold;") self.main_layout.addWidget(self.title) @@ -85,43 +116,43 @@ class LoginView(QWidget): def init_login_form(self): self.login_input = QLineEdit() - self.login_input.setPlaceholderText("Логин") + self.login_input.setPlaceholderText(localizer.translate("Логин")) self.login_input.setFixedHeight(40) self.password_input = QLineEdit() - self.password_input.setPlaceholderText("Пароль") + self.password_input.setPlaceholderText(localizer.translate("Пароль")) self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setFixedHeight(40) - self.login_button = QPushButton("Войти") + self.login_button = QPushButton(localizer.translate("Войти")) self.login_button.setFixedHeight(40) self.login_button.clicked.connect(self.handle_login) - self.register_switch = QPushButton("Нет аккаунта? Регистрация") + self.register_switch = QPushButton(localizer.translate("Нет аккаунта? Регистрация")) self.register_switch.setFlat(True) self.register_switch.clicked.connect(self.show_register_form) def init_register_form(self): - self.name_input = ValidationInput("Имя") - self.name_input.set_validator(validate_name, is_required=False) + # self.name_input = ValidationInput("Имя") + # self.name_input.set_validator(validate_name, is_required=False) - self.reg_login_input = ValidationInput("Логин") + self.reg_login_input = ValidationInput(localizer.translate("Логин")) self.reg_login_input.set_validator(validate_username) - self.reg_password_input = ValidationInput("Пароль", is_password=True) + self.reg_password_input = ValidationInput(localizer.translate("Пароль"), is_password=True) self.reg_password_input.set_validator(validate_password) - self.confirm_password_input = ValidationInput("Повторите пароль", is_password=True) + self.confirm_password_input = ValidationInput(localizer.translate("Повторите пароль"), is_password=True) self.confirm_password_input.set_validator(self.validate_confirm_password) - self.invite_code_input = ValidationInput("Инвайт-код") + self.invite_code_input = ValidationInput(localizer.translate("Инвайт-код")) self.invite_code_input.set_validator(validate_invite_code, is_required=False) - self.register_button = QPushButton("Зарегистрироваться") + self.register_button = QPushButton(localizer.translate("Зарегистрироваться")) self.register_button.setFixedHeight(40) self.register_button.clicked.connect(self.handle_register) - self.login_switch = QPushButton("Уже есть аккаунт? Войти") + self.login_switch = QPushButton(localizer.translate("Уже есть аккаунт? Войти")) self.login_switch.setFlat(True) self.login_switch.clicked.connect(self.show_login_form) @@ -129,7 +160,7 @@ class LoginView(QWidget): def show_login_form(self): self.is_registration = False - self.title.setText("Авторизация") + self.title.setText(localizer.translate("Авторизация")) self.clear_form() # Очистка layout @@ -143,12 +174,12 @@ class LoginView(QWidget): def show_register_form(self): self.is_registration = True - self.title.setText("Регистрация") + self.title.setText(localizer.translate("Регистрация")) self.clear_form() self.clear_main_layout() - self.main_layout.addWidget(self.name_input) + # self.main_layout.addWidget(self.name_input) self.main_layout.addWidget(self.reg_login_input) self.main_layout.addWidget(self.reg_password_input) self.main_layout.addWidget(self.confirm_password_input) @@ -160,7 +191,7 @@ class LoginView(QWidget): def clear_form(self): self.login_input.clear() self.password_input.clear() - self.name_input.clear() + # self.name_input.clear() self.reg_login_input.clear() self.reg_password_input.clear() self.confirm_password_input.clear() @@ -182,45 +213,51 @@ class LoginView(QWidget): if not is_login_valid or not is_password_valid: # Показываем первую попавшуюся ошибку, они должны быть общими error_msg = login_msg if not is_login_valid else password_msg - QMessageBox.warning(self, "Ошибка", error_msg) + QMessageBox.warning(self, localizer.translate("Ошибка"), error_msg) return if login == "root" and password == "123": self.on_login(login) else: - QMessageBox.warning(self, "Ошибка", "Неверный логин или пароль") + QMessageBox.warning(self, localizer.translate("Ошибка"), localizer.translate("Неверный логин или пароль")) def validate_confirm_password(self, text): if text != self.reg_password_input.text(): - return False, "Пароли не совпадают" + return False, localizer.translate("Пароли не совпадают") return True, "" def handle_register(self): # Trigger validation for all fields - self.name_input.on_text_changed(self.name_input.text()) + # self.name_input.on_text_changed(self.name_input.text()) self.reg_login_input.on_text_changed(self.reg_login_input.text()) self.reg_password_input.on_text_changed(self.reg_password_input.text()) self.confirm_password_input.on_text_changed(self.confirm_password_input.text()) self.invite_code_input.on_text_changed(self.invite_code_input.text()) if not all([ - self.name_input.is_valid, + # self.name_input.is_valid, self.reg_login_input.is_valid, self.reg_password_input.is_valid, self.confirm_password_input.is_valid, self.invite_code_input.is_valid ]): - QMessageBox.warning(self, "Ошибка", "Пожалуйста, исправьте ошибки в форме") + QMessageBox.warning(self, localizer.translate("Ошибка"), localizer.translate("Пожалуйста, исправьте ошибки в форме")) return - name = self.name_input.text() - # login = self.reg_login_input.text() + # name = self.name_input.text() + login = self.reg_login_input.text() # password = self.reg_password_input.text() # invite = self.invite_code_input.text() - QMessageBox.information(self, "Успех", f"Регистрация прошла успешно для {name}") + QMessageBox.information(self, "Успех", f"{localizer.translate("Регистрация прошла успешно для")} {login}") self.show_login_form() + def change_language(self, display_name): + lang_code = self.lang_map.get(display_name) + if lang_code: + localizer.switch_language(lang_code) + self.update_ui_language() + def toggle_theme(self): self.is_dark_theme = not self.is_dark_theme if self.is_dark_theme: @@ -287,3 +324,21 @@ class LoginView(QWidget): } """) self.theme_button.setText("🌞") + + def update_ui_language(self): + self.theme_button.setText("🌞" if not self.is_dark_theme else "🌙") + + if self.is_registration: + self.title.setText(localizer.translate("Регистрация")) + self.register_button.setText(localizer.translate("Зарегистрироваться")) + self.login_switch.setText(localizer.translate("Уже есть аккаунт? Войти")) + self.reg_login_input.set_label(localizer.translate("Логин")) + self.reg_password_input.set_label(localizer.translate("Пароль")) + self.confirm_password_input.set_label(localizer.translate("Повторите пароль")) + self.invite_code_input.set_label(localizer.translate("Инвайт-код")) + else: + self.title.setText(localizer.translate("Авторизация")) + self.login_button.setText(localizer.translate("Войти")) + self.register_switch.setText(localizer.translate("Нет аккаунта? Регистрация")) + self.login_input.setPlaceholderText(localizer.translate("Логин")) + self.password_input.setPlaceholderText(localizer.translate("Пароль"))