image-lab / src /ui /components.py
Magnus Persson
Two-column Create layout with fixed slots, clearer labels, and a How it works pipeline tab
4fc023c
Raw History Blame Contribute Delete
2.29 kB
"""Helpers that keep every visible string in the locale files and switchable at runtime."""
from __future__ import annotations
import io
from collections.abc import Callable, Iterable
from typing import Any, Literal
import gradio as gr
from PIL import Image
from src.i18n import I18n
Block = gr.blocks.Block
Updates = dict[Block, dict[str, Any]]
Props = Callable[[str], dict[str, Any]]
ApiVisibility = Literal["private", "undocumented"]
class Localizer:
"""Creates components from locale-driven props and re-renders them for another language."""
def __init__(self, i18n: I18n) -> None:
self.i18n = i18n
self.default_language = i18n.default_language
self._bindings: list[tuple[Block, Props]] = []
def t(self, lang: str, key: str, **values: object) -> str:
return self.i18n.t(lang, key, **values)
def make(self, cls: type[Block], props: Props, **static: Any) -> Any:
"""Build `cls` with props for the default language and remember how to re-localize it."""
component = cls(**props(self.default_language), **static)
self._bindings.append((component, props))
return component
@property
def components(self) -> list[Block]:
return [component for component, _ in self._bindings]
def props(self, lang: str) -> Updates:
return {component: props(lang) for component, props in self._bindings}
def build_heading(loc: Localizer, title_key: str, hint_key: str) -> Any:
"""A bold section title with a one-line explanation under it."""
return loc.make(
gr.Markdown,
lambda lang: {"value": f"**{loc.t(lang, title_key)}** \n{loc.t(lang, hint_key)}"},
elem_classes=["section-heading"],
)
def merge(*parts: Updates) -> Updates:
merged: Updates = {}
for part in parts:
for component, props in part.items():
merged[component] = {**merged.get(component, {}), **props}
return merged
def pack(order: Iterable[Block], updates: Updates) -> list[Any]:
"""Turn updates into an output list aligned with `order`; untouched components get no-ops."""
return [gr.update(**updates[c]) if c in updates else gr.skip() for c in order]
def to_pil(image: bytes) -> Image.Image:
return Image.open(io.BytesIO(image))