feat: add translation files and i18n loader

Create TOML-based localisation resources under app/lang and introduce a Translator/I18nMixin that reads them. Update config handling to recognise available languages, switch UI strings to translation lookups, and bundle language files with the package.
This commit is contained in:
lm
2025-10-17 16:43:25 +02:00
parent 76073ab0b5
commit 91fad62808
13 changed files with 442 additions and 100 deletions
+29 -12
View File
@@ -15,15 +15,21 @@ class ColorPickerMixin:
selected_colour: tuple[int, int, int] | None = None
def choose_color(self):
rgb, hex_colour = colorchooser.askcolor(title="Farbe wählen")
title = self._t("dialog.choose_colour_title") if hasattr(self, "_t") else "Choose colour"
rgb, hex_colour = colorchooser.askcolor(title=title)
if rgb is None:
return
r, g, b = (int(round(channel)) for channel in rgb)
hue_deg, sat_pct, val_pct = self._apply_rgb_selection(r, g, b)
label = hex_colour or f"RGB({r}, {g}, {b})"
self.status.config(
text=f"Farbe gewählt: {label} — Hue {hue_deg:.1f}°, S {sat_pct:.0f}%, V {val_pct:.0f}%"
message = self._t(
"status.color_selected",
label=label,
hue=hue_deg,
saturation=sat_pct,
value=val_pct,
)
self.status.config(text=message)
self._update_selected_colour(r, g, b)
def apply_sample_colour(self, hex_colour: str, name: str | None = None) -> None:
@@ -35,25 +41,31 @@ class ColorPickerMixin:
if self.pick_mode:
self.pick_mode = False
label = name or hex_colour.upper()
self.status.config(
text=(
f"Beispielfarbe gewählt: {label} ({hex_colour}) — "
f"Hue {hue_deg:.1f}°, S {sat_pct:.0f}%, V {val_pct:.0f}%"
)
message = self._t(
"status.sample_colour",
label=label,
hex_code=hex_colour,
hue=hue_deg,
saturation=sat_pct,
value=val_pct,
)
self.status.config(text=message)
self._update_selected_colour(*rgb)
def enable_pick_mode(self):
if self.preview_img is None:
messagebox.showinfo("Info", "Bitte zuerst ein Bild laden.")
messagebox.showinfo(
self._t("dialog.info_title"),
self._t("dialog.load_image_first"),
)
return
self.pick_mode = True
self.status.config(text="Pick-Modus: Klicke links ins Bild, um Farbe zu wählen (Esc beendet)")
self.status.config(text=self._t("status.pick_mode_ready"))
def disable_pick_mode(self, event=None):
if self.pick_mode:
self.pick_mode = False
self.status.config(text="Pick-Modus beendet.")
self.status.config(text=self._t("status.pick_mode_ended"))
def on_canvas_click(self, event):
if not self.pick_mode or self.preview_img is None:
@@ -68,7 +80,12 @@ class ColorPickerMixin:
hue_deg, sat_pct, val_pct = self._apply_rgb_selection(r, g, b)
self.disable_pick_mode()
self.status.config(
text=f"Farbe vom Bild gewählt: Hue {hue_deg:.1f}°, S {sat_pct:.0f}%, V {val_pct:.0f}%"
text=self._t(
"status.pick_mode_from_image",
hue=hue_deg,
saturation=sat_pct,
value=val_pct,
)
)
self._update_selected_colour(r, g, b)
+4
View File
@@ -67,6 +67,10 @@ class ThemeMixin:
if callable(accent_refresher) and hasattr(self, "filename_label"):
accent_refresher(highlight_fg)
canvas_refresher = getattr(self, "_refresh_canvas_backgrounds", None)
if callable(canvas_refresher):
canvas_refresher()
def detect_system_theme(self) -> str:
"""Best-effort detection of the OS theme preference."""
try:
+84 -60
View File
@@ -17,15 +17,15 @@ class UIBuilderMixin:
toolbar = ttk.Frame(self.root)
toolbar.pack(fill=tk.X, padx=12, pady=(4, 2))
buttons = [
("📂", "Bild laden", self.load_image),
("📁", "Ordner laden", self.load_folder),
("🎨", "Farbe wählen", self.choose_color),
("🖱", "Farbe aus Bild klicken", self.enable_pick_mode),
("💾", "Overlay speichern", self.save_overlay),
("🧹", "Ausschlüsse löschen", self.clear_excludes),
("", "Letzten Ausschluss entfernen", self.undo_exclude),
("🔄", "Slider zurücksetzen", self.reset_sliders),
("🌓", "Theme umschalten", self.toggle_theme),
("📂", self._t("toolbar.open_image"), self.load_image),
("📁", self._t("toolbar.open_folder"), self.load_folder),
("🎨", self._t("toolbar.choose_color"), self.choose_color),
("🖱", self._t("toolbar.pick_from_image"), self.enable_pick_mode),
("💾", self._t("toolbar.save_overlay"), self.save_overlay),
("🧹", self._t("toolbar.clear_excludes"), self.clear_excludes),
("", self._t("toolbar.undo_exclude"), self.undo_exclude),
("🔄", self._t("toolbar.reset_sliders"), self.reset_sliders),
("🌓", self._t("toolbar.toggle_theme"), self.toggle_theme),
]
self._toolbar_buttons: list[dict[str, object]] = []
self._nav_buttons: list[tk.Button] = []
@@ -39,10 +39,10 @@ class UIBuilderMixin:
status_container.pack(side=tk.RIGHT, expand=True, fill=tk.X)
self.status = ttk.Label(
status_container,
text="Keine Datei geladen.",
anchor="e",
foreground="#efefef",
)
text=self._t("status.no_file"),
anchor="e",
foreground="#efefef",
)
self.status.pack(fill=tk.X)
self._attach_copy_menu(self.status)
self.status_default_text = self.status.cget("text")
@@ -54,7 +54,7 @@ class UIBuilderMixin:
current_frame = ttk.Frame(palette_frame)
current_frame.pack(side=tk.LEFT, padx=(0, 16))
ttk.Label(current_frame, text="Farbe:").pack(side=tk.LEFT, padx=(0, 6))
ttk.Label(current_frame, text=self._t("palette.current")).pack(side=tk.LEFT, padx=(0, 6))
self.current_colour_sw = tk.Canvas(
current_frame,
width=24,
@@ -67,7 +67,7 @@ class UIBuilderMixin:
self.current_colour_label = ttk.Label(current_frame, text=f"({default_colour})")
self.current_colour_label.pack(side=tk.LEFT, padx=(6, 0))
ttk.Label(palette_frame, text="Weitere Farben:").pack(side=tk.LEFT, padx=(0, 8))
ttk.Label(palette_frame, text=self._t("palette.more")).pack(side=tk.LEFT, padx=(0, 8))
swatch_container = ttk.Frame(palette_frame)
swatch_container.pack(side=tk.LEFT)
for name, hex_code in self._preset_colours():
@@ -75,14 +75,14 @@ class UIBuilderMixin:
sliders_frame = ttk.Frame(self.root)
sliders_frame.pack(fill=tk.X, padx=12, pady=4)
sliders = [
("Hue Min (°)", self.hue_min, 0, 360),
("Hue Max (°)", self.hue_max, 0, 360),
("Sättigung Min (%)", self.sat_min, 0, 100),
("Helligkeit Min (%)", self.val_min, 0, 100),
("Helligkeit Max (%)", self.val_max, 0, 100),
("Overlay Alpha", self.alpha, 0, 255),
]
sliders = [
(self._t("sliders.hue_min"), self.hue_min, 0, 360),
(self._t("sliders.hue_max"), self.hue_max, 0, 360),
(self._t("sliders.sat_min"), self.sat_min, 0, 100),
(self._t("sliders.val_min"), self.val_min, 0, 100),
(self._t("sliders.val_max"), self.val_max, 0, 100),
(self._t("sliders.alpha"), self.alpha, 0, 255),
]
for index, (label, variable, minimum, maximum) in enumerate(sliders):
self.add_slider_with_value(sliders_frame, label, variable, minimum, maximum, column=index)
sliders_frame.grid_columnconfigure(index, weight=1)
@@ -97,7 +97,12 @@ class UIBuilderMixin:
self._create_navigation_button(left_column, "", self.show_previous_image, column=0)
self.canvas_orig = tk.Canvas(left_column, bg="#1e1e1e", highlightthickness=0, relief="flat")
self.canvas_orig = tk.Canvas(
left_column,
bg=self._canvas_background_colour(),
highlightthickness=0,
relief="flat",
)
self.canvas_orig.grid(row=0, column=1, sticky="nsew")
self.canvas_orig.bind("<Button-1>", self.on_canvas_click)
self.canvas_orig.bind("<ButtonPress-3>", self._exclude_start)
@@ -109,7 +114,12 @@ class UIBuilderMixin:
right_column.grid_columnconfigure(0, weight=1)
right_column.grid_rowconfigure(0, weight=1)
self.canvas_overlay = tk.Canvas(right_column, bg="#1e1e1e", highlightthickness=0, relief="flat")
self.canvas_overlay = tk.Canvas(
right_column,
bg=self._canvas_background_colour(),
highlightthickness=0,
relief="flat",
)
self.canvas_overlay.grid(row=0, column=0, sticky="nsew")
self._create_navigation_button(right_column, "", self.show_next_image, column=1)
@@ -128,7 +138,7 @@ class UIBuilderMixin:
self.ratio_label = ttk.Label(
info_frame,
text="Markierungen (mit Ausschlüssen): —",
text=self._t("stats.placeholder"),
font=("Segoe UI", 10, "bold"),
anchor="center",
justify="center",
@@ -177,20 +187,20 @@ class UIBuilderMixin:
pass
self._update_job = self.root.after(self.update_delay_ms, self.update_preview)
def _preset_colours(self):
return [
("Rot", "#ff3b30"),
("Orange", "#ff9500"),
("Gelb", "#ffd60a"),
("Grün", "#34c759"),
("Türkis", "#5ac8fa"),
("Blau", "#0a84ff"),
("Violett", "#af52de"),
("Magenta", "#ff2d55"),
("Weiß", "#ffffff"),
("Grau", "#8e8e93"),
("Schwarz", "#000000"),
]
def _preset_colours(self):
return [
(self._t("palette.swatch.red"), "#ff3b30"),
(self._t("palette.swatch.orange"), "#ff9500"),
(self._t("palette.swatch.yellow"), "#ffd60a"),
(self._t("palette.swatch.green"), "#34c759"),
(self._t("palette.swatch.teal"), "#5ac8fa"),
(self._t("palette.swatch.blue"), "#0a84ff"),
(self._t("palette.swatch.violet"), "#af52de"),
(self._t("palette.swatch.magenta"), "#ff2d55"),
(self._t("palette.swatch.white"), "#ffffff"),
(self._t("palette.swatch.grey"), "#8e8e93"),
(self._t("palette.swatch.black"), "#000000"),
]
def _add_palette_swatch(self, parent, name: str, hex_code: str) -> None:
swatch = tk.Canvas(
@@ -401,14 +411,14 @@ class UIBuilderMixin:
logo_label.image = logo # keep reference
logo_label.pack(side=tk.LEFT, padx=(10, 6), pady=4)
title_label = tk.Label(
title_bar,
text="Interactive Color Range Analyzer",
bg=bar_bg,
fg="#f5f5f5",
font=("Segoe UI", 11, "bold"),
anchor="w",
)
title_label = tk.Label(
title_bar,
text=self._t("app.title"),
bg=bar_bg,
fg="#f5f5f5",
font=("Segoe UI", 11, "bold"),
anchor="w",
)
title_label.pack(side=tk.LEFT, padx=6)
close_btn = tk.Button(
@@ -499,18 +509,31 @@ class UIBuilderMixin:
for text_id in text_ids:
canvas.itemconfigure(text_id, fill=palette["text"])
def _refresh_navigation_buttons_theme(self) -> None:
if not getattr(self, "_nav_buttons", None):
return
palette = self._navigation_palette()
for btn in self._nav_buttons:
def _refresh_navigation_buttons_theme(self) -> None:
if not getattr(self, "_nav_buttons", None):
return
palette = self._navigation_palette()
for btn in self._nav_buttons:
btn.configure(
background=palette["bg"],
activebackground=palette["bg"],
fg=palette["fg"],
activeforeground=palette["fg"],
)
activeforeground=palette["fg"],
)
def _canvas_background_colour(self) -> str:
return "#0f0f10" if getattr(self, "theme", "light") == "dark" else "#ffffff"
def _refresh_canvas_backgrounds(self) -> None:
bg = self._canvas_background_colour()
for attr in ("canvas_orig", "canvas_overlay"):
canvas = getattr(self, attr, None)
if canvas is not None:
try:
canvas.configure(bg=bg)
except Exception:
pass
def _refresh_status_palette(self, fg: str) -> None:
self.status.configure(foreground=fg)
self._status_palette["fg"] = fg
@@ -542,10 +565,11 @@ class UIBuilderMixin:
r, g, b = colorsys.hsv_to_rgb(hue / 360.0, saturation / 100.0, value / 100.0)
return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
def _init_copy_menu(self):
self._copy_target = None
self.copy_menu = tk.Menu(self.root, tearoff=0)
self.copy_menu.add_command(label="Kopieren", command=self._copy_current_label)
def _init_copy_menu(self):
self._copy_target = None
self.copy_menu = tk.Menu(self.root, tearoff=0)
label = self._t("menu.copy") if hasattr(self, "_t") else "Copy"
self.copy_menu.add_command(label=label, command=self._copy_current_label)
def _attach_copy_menu(self, widget):
widget.bind("<Button-3>", lambda event, w=widget: self._show_copy_menu(event, w))