4 Commits

Author SHA1 Message Date
a5df7f0477 chore(changelog): update
All checks were successful
Code and build check / Check code (push) Successful in 1m35s
Code and build check / Build with uv (push) Successful in 58s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 13:19:13 +05:00
f2954497d9 chore(readme): update todo
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 13:18:04 +05:00
80bbab692d chore(documentation): mention localization in custom data
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 13:17:07 +05:00
731e919884 feat: added translate support to custom data
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 13:10:37 +05:00
7 changed files with 59 additions and 31 deletions

View File

@@ -6,6 +6,7 @@
## [Unreleased] ## [Unreleased]
### Added ### Added
- Переводы в переопределениях (за подробностями в документацию)
### Changed ### Changed

View File

@@ -34,7 +34,7 @@
- [ ] Достигнуть паритета функциональности с PortProton - [ ] Достигнуть паритета функциональности с PortProton
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}` - [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/) - [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
- [ ] Добавить переводы в переопределения - [X] Добавить переводы в переопределения
- [ ] Придумать как переопределять launcher.exe - [ ] Придумать как переопределять launcher.exe
- [X] Добавить в карточку игры сведения о поддержке геймпада - [X] Добавить в карточку игры сведения о поддержке геймпада
- [X] Добавить в карточки данные с ProtonDB - [X] Добавить в карточки данные с ProtonDB

View File

@@ -50,7 +50,9 @@ Each `<exe_name>` folder can include:
- `metadata.txt` — contains name and description: - `metadata.txt` — contains name and description:
```txt ```txt
name=My Game Title name=My Game Title
name_ru=My Game Title (in russian language)
description=My Game Description description=My Game Description
description_ru=My Game Description (in russian language)
``` ```
- `cover.<extension>` — image file (`.png`, `.jpg`, `.jpeg`, `.bmp`) - `cover.<extension>` — image file (`.png`, `.jpg`, `.jpeg`, `.bmp`)

View File

@@ -50,7 +50,9 @@
- `metadata.txt` — имя и описание в формате: - `metadata.txt` — имя и описание в формате:
```txt ```txt
name=Моё название игры name=Моё название игры
description=Описание моей игры name_en=Моё название игры (на английском)
description=Описание моей игры (на английском)
description_en=Описание моей игры
``` ```
- `cover.<расширение>` — обложка (`.png`, `.jpg`, `.jpeg`, `.bmp`) - `cover.<расширение>` — обложка (`.png`, `.jpg`, `.jpeg`, `.bmp`)

View File

@@ -1,6 +1,7 @@
import gettext import gettext
from pathlib import Path from pathlib import Path
import locale import locale
import os
from babel import Locale from babel import Locale
LOCALE_MAP = { LOCALE_MAP = {
@@ -72,3 +73,32 @@ def get_egs_language():
# Если что-то пошло не так — используем английский по умолчанию # Если что-то пошло не так — используем английский по умолчанию
return 'en' return 'en'
def read_metadata_translations(metadata_file, language_code):
"""
Читает переводы из metadata.txt для указанного языка.
Возвращает словарь с полями name и description.
Для name: использует name_<language_code>, затем name_en, затем name, и наконец _('Unknown Game').
Для description: использует description_<language_code>, затем description_en, затем description.
"""
translations = {'name': _('Unknown Game'), 'description': ''}
if not os.path.exists(metadata_file):
return translations
with open(metadata_file, encoding='utf-8') as f:
for line in f:
line = line.strip()
if line.startswith(f'name_{language_code}='):
translations['name'] = line[len(f'name_{language_code}='):].strip()
elif line.startswith('name_en=') and translations['name'] == _('Unknown Game'):
translations['name'] = line[len('name_en='):].strip()
elif line.startswith('name=') and translations['name'] == _('Unknown Game'):
translations['name'] = line[len('name='):].strip()
elif line.startswith(f'description_{language_code}='):
translations['description'] = line[len(f'description_{language_code}='):].strip()
elif line.startswith('description_en=') and not translations['description']:
translations['description'] = line[len('description_en='):].strip()
elif line.startswith('description=') and not translations['description']:
translations['description'] = line[len('description='):].strip()
return translations

View File

@@ -28,7 +28,7 @@ from portprotonqt.config_utils import (
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config, save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config
) )
from portprotonqt.localization import _ from portprotonqt.localization import _, get_egs_language, read_metadata_translations
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
from portprotonqt.downloader import Downloader from portprotonqt.downloader import Downloader
@@ -465,11 +465,9 @@ class MainWindow(QMainWindow):
os.makedirs(user_custom_folder, exist_ok=True) os.makedirs(user_custom_folder, exist_ok=True)
builtin_cover = "" builtin_cover = ""
builtin_name = None
builtin_desc = None
user_cover = "" user_cover = ""
user_name = None user_game_folder=""
user_desc = None builtin_game_folder=""
if game_exe: if game_exe:
exe_name = os.path.splitext(os.path.basename(game_exe))[0] exe_name = os.path.splitext(os.path.basename(game_exe))[0]
@@ -477,6 +475,7 @@ class MainWindow(QMainWindow):
user_game_folder = os.path.join(user_custom_folder, exe_name) user_game_folder = os.path.join(user_custom_folder, exe_name)
os.makedirs(user_game_folder, exist_ok=True) os.makedirs(user_game_folder, exist_ok=True)
# Чтение обложки
builtin_files = set(os.listdir(builtin_game_folder)) if os.path.exists(builtin_game_folder) else set() builtin_files = set(os.listdir(builtin_game_folder)) if os.path.exists(builtin_game_folder) else set()
for ext in [".jpg", ".png", ".jpeg", ".bmp"]: for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
candidate = f"cover{ext}" candidate = f"cover{ext}"
@@ -484,16 +483,6 @@ class MainWindow(QMainWindow):
builtin_cover = os.path.join(builtin_game_folder, candidate) builtin_cover = os.path.join(builtin_game_folder, candidate)
break break
builtin_metadata_file = os.path.join(builtin_game_folder, "metadata.txt")
if os.path.exists(builtin_metadata_file):
with open(builtin_metadata_file, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("name="):
builtin_name = line[len("name="):].strip()
elif line.startswith("description="):
builtin_desc = line[len("description="):].strip()
user_files = set(os.listdir(user_game_folder)) if os.path.exists(user_game_folder) else set() user_files = set(os.listdir(user_game_folder)) if os.path.exists(user_game_folder) else set()
for ext in [".jpg", ".png", ".jpeg", ".bmp"]: for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
candidate = f"cover{ext}" candidate = f"cover{ext}"
@@ -501,16 +490,7 @@ class MainWindow(QMainWindow):
user_cover = os.path.join(user_game_folder, candidate) user_cover = os.path.join(user_game_folder, candidate)
break break
user_metadata_file = os.path.join(user_game_folder, "metadata.txt") # Чтение статистики
if os.path.exists(user_metadata_file):
with open(user_metadata_file, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("name="):
user_name = line[len("name="):].strip()
elif line.startswith("description="):
user_desc = line[len("description="):].strip()
if self.portproton_location: if self.portproton_location:
statistics_file = os.path.join(self.portproton_location, "data", "tmp", "statistics") statistics_file = os.path.join(self.portproton_location, "data", "tmp", "statistics")
try: try:
@@ -526,13 +506,26 @@ class MainWindow(QMainWindow):
print(f"Failed to parse playtime data: {e}") print(f"Failed to parse playtime data: {e}")
def on_steam_info(steam_info: dict): def on_steam_info(steam_info: dict):
final_name = user_name or builtin_name or desktop_name # Определяем текущий язык
final_desc = (user_desc if user_desc is not None else language_code = get_egs_language()
builtin_desc if builtin_desc is not None else
steam_info.get("description", "")) # Чтение переводов из metadata.txt
user_metadata_file = os.path.join(user_game_folder, "metadata.txt")
builtin_metadata_file = os.path.join(builtin_game_folder, "metadata.txt")
# Сначала пытаемся загрузить пользовательские переводы
translations = {'name': desktop_name, 'description': ''}
if os.path.exists(user_metadata_file):
translations = read_metadata_translations(user_metadata_file, language_code)
elif os.path.exists(builtin_metadata_file):
translations = read_metadata_translations(builtin_metadata_file, language_code)
final_name = translations['name']
final_desc = translations['description'] or steam_info.get("description", "")
final_cover = (user_cover if user_cover else final_cover = (user_cover if user_cover else
builtin_cover if builtin_cover else builtin_cover if builtin_cover else
steam_info.get("cover", "") or entry.get("Icon", "")) steam_info.get("cover", "") or entry.get("Icon", ""))
callback(( callback((
final_name, final_name,
final_desc, final_desc,