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:
+29
-12
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user