Загрузка данных
import json
import os
import tkinter as tk
from tkinter import messagebox, ttk
CONFIG_FILE = "config.json"
DEFAULT_LANG = "ru"
WEIGHT_LIMIT = 50.0
class InventoryApp(tk.Tk):
def __init__(self):
super().__init__()
self.geometry("820x650")
self.minsize(750, 580)
# 1. Загрузка конфигурации
self.current_lang = self.load_config()
# 2. Загрузка локалей и аудит полноты перевода
self.locales = {}
self.reference_keys = set()
self.localization_warnings = []
self.load_all_translations()
# Данные инвентаря: храним список словарей
# {'name': str, 'cat_key': str, 'weight': float, 'qty': int}
self.inventory = [
{
"name": "Healing Potion",
"cat_key": "cat_potion",
"weight": 0.5,
"qty": 3,
},
{
"name": "Iron Sword",
"cat_key": "cat_weapon",
"weight": 5.0,
"qty": 1,
},
]
# 3. Построение GUI
self.create_widgets()
# 4. Вывод ошибок полноты локализации в лог
for warn in self.localization_warnings:
self.log_message(warn)
# 5. Применение текущего языка
self.apply_language()
def load_config(self):
"""Чтение config.json или создание дефолтного."""
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
cfg = json.load(f)
return cfg.get("language", DEFAULT_LANG)
except Exception:
return DEFAULT_LANG
else:
self.save_config(DEFAULT_LANG)
return DEFAULT_LANG
def save_config(self, lang_code):
try:
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump({"language": lang_code}, f, indent=4)
except Exception as e:
print(f"Error saving config: {e}")
def load_all_translations(self):
"""Загрузка всех json и аудит эталонных ключей."""
langs = ["ru", "en"]
for l in langs:
filename = f"{l}.json"
if os.path.exists(filename):
try:
with open(filename, "r", encoding="utf-8") as f:
self.locales[l] = json.load(f)
except Exception as e:
self.locales[l] = {}
self.localization_warnings.append(
f"[LOCALIZATION] Ошибка чтения {filename}: {e}"
)
else:
self.locales[l] = {}
self.localization_warnings.append(
f"[LOCALIZATION] Файл не найден: {filename}"
)
# Построение эталона (объединение всех ключей)
for d in self.locales.values():
self.reference_keys.update(d.keys())
# Проверка полноты перевода для каждого файла
for l, d in self.locales.items():
missing = self.reference_keys - set(d.keys())
for m in missing:
self.localization_warnings.append(
f"[LOCALIZATION] Отсутствует строка: {m} в файле {l}.json"
)
def tr(self, key, **kwargs):
"""Получение перевода с fallback на [MISSING: key]."""
active_dict = self.locales.get(self.current_lang, {})
if key not in active_dict:
return f"[MISSING: {key}]"
text = active_dict[key]
if kwargs:
try:
return text.format(**kwargs)
except Exception:
return text
return text
def create_widgets(self):
# Главное меню
self.menubar = tk.Menu(self)
self.config(menu=self.menubar)
self.menu_inv = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(menu=self.menu_inv)
self.menu_inv.add_command(command=self.quit)
self.menu_char = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(menu=self.menu_char)
self.menu_char.add_command(command=self.show_char_profile)
self.menu_char.add_command(command=self.reset_inventory)
self.menu_lang = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(menu=self.menu_lang)
self.menu_lang.add_command(
label="Русский (RU)", command=lambda: self.switch_language("ru")
)
self.menu_lang.add_command(
label="English (EN)", command=lambda: self.switch_language("en")
)
# Верхняя панель: форма ввода предметов
self.frame_input = ttk.LabelFrame(self, padding=10)
self.frame_input.pack(fill="x", padx=10, pady=5)
self.lbl_name = ttk.Label(self.frame_input)
self.lbl_name.grid(row=0, column=0, sticky="w", padx=5, pady=2)
self.entry_name = ttk.Entry(self.frame_input, width=18)
self.entry_name.grid(row=0, column=1, padx=5, pady=2)
self.lbl_cat = ttk.Label(self.frame_input)
self.lbl_cat.grid(row=0, column=2, sticky="w", padx=5, pady=2)
self.combo_cat = ttk.Combobox(
self.frame_input, state="readonly", width=14
)
self.combo_cat.grid(row=0, column=3, padx=5, pady=2)
self.lbl_weight = ttk.Label(self.frame_input)
self.lbl_weight.grid(row=1, column=0, sticky="w", padx=5, pady=2)
self.entry_weight = ttk.Entry(self.frame_input, width=18)
self.entry_weight.grid(row=1, column=1, padx=5, pady=2)
self.lbl_qty = ttk.Label(self.frame_input)
self.lbl_qty.grid(row=1, column=2, sticky="w", padx=5, pady=2)
self.entry_qty = ttk.Entry(self.frame_input, width=14)
self.entry_qty.grid(row=1, column=3, padx=5, pady=2)
self.btn_add = ttk.Button(self.frame_input, command=self.add_item)
self.btn_add.grid(
row=0, column=4, rowspan=2, padx=15, pady=2, sticky="nsew"
)
# Таблица предметов (Treeview)
self.frame_table = ttk.Frame(self, padding=10)
self.frame_table.pack(fill="both", expand=True, padx=10, pady=5)
self.tree = ttk.Treeview(
self.frame_table,
columns=("name", "cat", "weight", "qty", "total"),
show="headings",
selectmode="browse",
)
self.tree.pack(side="left", fill="both", expand=True)
scrollbar = ttk.Scrollbar(
self.frame_table, orient="vertical", command=self.tree.yview
)
self.tree.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side="right", fill="y")
# Панель действий под таблицей и статистика
self.frame_actions = ttk.Frame(self, padding=10)
self.frame_actions.pack(fill="x", padx=10, pady=2)
self.btn_use = ttk.Button(self.frame_actions, command=self.use_item)
self.btn_use.pack(side="left", padx=5)
self.btn_delete = ttk.Button(
self.frame_actions, command=self.delete_item
)
self.btn_delete.pack(side="left", padx=5)
self.lbl_stats = ttk.Label(
self.frame_actions, font=("Helvetica", 10, "bold")
)
self.lbl_stats.pack(side="right", padx=10)
# Поле лога
self.frame_log = ttk.LabelFrame(self, padding=5)
self.frame_log.pack(fill="x", padx=10, pady=5)
self.txt_log = tk.Text(
self.frame_log, height=5, state="disabled", wrap="word", bg="#f5f5f5"
)
self.txt_log.pack(fill="x", padx=5, pady=2)
def apply_language(self):
"""Централизованное обновление всех элементов интерфейса."""
self.title(self.tr("app_title"))
# Меню
self.menubar.entryconfig(1, label=self.tr("menu_inventory"))
self.menubar.entryconfig(2, label=self.tr("menu_character"))
self.menubar.entryconfig(3, label=self.tr("menu_language"))
self.menu_inv.entryconfig(0, label=self.tr("menu_exit"))
self.menu_char.entryconfig(0, label=self.tr("menu_char_profile"))
self.menu_char.entryconfig(1, label=self.tr("menu_char_reset"))
# Лейблы и кнопки
self.lbl_name.config(text=self.tr("lbl_item_name"))
self.lbl_cat.config(text=self.tr("lbl_category"))
self.lbl_weight.config(text=self.tr("lbl_weight"))
self.lbl_qty.config(text=self.tr("lbl_qty"))
self.btn_add.config(text=self.tr("btn_add"))
self.btn_use.config(text=self.tr("btn_use"))
self.btn_delete.config(text=self.tr("btn_delete"))
self.frame_log.config(text=self.tr("log_title"))
# Обновление выпадающего списка категорий с сохранением индекса
cat_keys = ["cat_weapon", "cat_armor", "cat_potion", "cat_misc"]
cat_labels = [self.tr(k) for k in cat_keys]
current_idx = self.combo_cat.current()
self.combo_cat["values"] = cat_labels
if current_idx >= 0 and current_idx < len(cat_labels):
self.combo_cat.current(current_idx)
else:
self.combo_cat.current(0)
# Заголовки колонок таблицы
self.tree.heading("name", text=self.tr("col_name"))
self.tree.heading("cat", text=self.tr("col_category"))
self.tree.heading("weight", text=self.tr("col_weight"))
self.tree.heading("qty", text=self.tr("col_qty"))
self.tree.heading("total", text=self.tr("col_total"))
# Перерисовка строк таблицы и перерасчет веса
self.refresh_table()
def switch_language(self, lang):
if self.current_lang != lang:
self.current_lang = lang
self.save_config(lang)
self.apply_language()
def log_message(self, message):
self.txt_log.config(state="normal")
self.txt_log.insert("end", message + "\n")
self.txt_log.see("end")
self.txt_log.config(state="disabled")
def refresh_table(self):
# Запоминаем текущий выбор
selected = self.tree.selection()
selected_idx = self.tree.index(selected[0]) if selected else None
for item in self.tree.get_children():
self.tree.delete(item)
total_weight = 0.0
for item in self.inventory:
cat_display = self.tr(item["cat_key"])
row_total = item["weight"] * item["qty"]
total_weight += row_total
self.tree.insert(
"",
"end",
values=(
item["name"],
cat_display,
f"{item['weight']:.2f}",
item["qty"],
f"{row_total:.2f}",
),
)
# Восстановление выбора
children = self.tree.get_children()
if (
selected_idx is not None
and children
and selected_idx < len(children)
):
self.tree.selection_set(children[selected_idx])
# Обновление метки статистики
self.lbl_stats.config(
text=self.tr(
"stat_total_weight",
total=f"{total_weight:.1f}",
limit=f"{WEIGHT_LIMIT:.1f}",
)
)
if total_weight > WEIGHT_LIMIT:
self.lbl_stats.config(foreground="red")
else:
self.lbl_stats.config(foreground="black")
def add_item(self):
name = self.entry_name.get().strip()
try:
weight = float(self.entry_weight.get().strip())
qty = int(self.entry_qty.get().strip())
if weight < 0 or qty <= 0 or not name:
raise ValueError()
except ValueError:
messagebox.showerror("Error", self.tr("err_invalid_input"))
return
cat_keys = ["cat_weapon", "cat_armor", "cat_potion", "cat_misc"]
cat_key = cat_keys[self.combo_cat.current()]
for item in self.inventory:
if item["name"].lower() == name.lower():
messagebox.showwarning("Notice", self.tr("err_item_exists"))
return
self.inventory.append(
{"name": name, "cat_key": cat_key, "weight": weight, "qty": qty}
)
self.refresh_table()
self.log_message(self.tr("log_item_added", name=name, qty=qty))
# Проверка перегруза
cur_weight = sum(i["weight"] * i["qty"] for i in self.inventory)
if cur_weight > WEIGHT_LIMIT:
self.log_message(self.tr("log_overweight_warning"))
# Очистка полей ввода
self.entry_name.delete(0, "end")
self.entry_weight.delete(0, "end")
self.entry_qty.delete(0, "end")
def use_item(self):
sel = self.tree.selection()
if not sel:
return
idx = self.tree.index(sel[0])
item = self.inventory[idx]
item["qty"] -= 1
self.log_message(self.tr("log_item_used", name=item["name"]))
if item["qty"] <= 0:
self.inventory.pop(idx)
self.refresh_table()
def delete_item(self):
sel = self.tree.selection()
if not sel:
return
idx = self.tree.index(sel[0])
item = self.inventory.pop(idx)
self.log_message(self.tr("log_item_deleted", name=item["name"]))
self.refresh_table()
def show_char_profile(self):
messagebox.showinfo(
self.tr("menu_char_profile"), self.tr("char_profile_info")
)
def reset_inventory(self):
self.inventory.clear()
self.refresh_table()
self.log_message(self.tr("log_weight_normal"))
if __name__ == "__main__":
app = InventoryApp()
app.mainloop()