GraphGen / webui /i18n.py
github-actions[bot]
Auto-sync from demo at Wed Sep 10 08:57:26 UTC 2025
4b2a9c2
raw
history blame
13.1 kB
# pylint: skip-file
import functools
import inspect
import json
import os
from contextlib import contextmanager
import gradio as gr
import langcodes
import yaml
from gradio.blocks import Block, BlockContext, Context, LocalContext
# Monkey patch to escape I18nString type being stripped in gradio.Markdown
def escape_caller(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if args and isinstance(args[0], I18nString):
add_values = args[0].add_values
radd_values = args[0].radd_values
result = I18nString(func(*args, **kwargs))
result.add_values = add_values
result.radd_values = radd_values
return result
return func(*args, **kwargs)
return wrapper
inspect.cleandoc = escape_caller(inspect.cleandoc)
class TranslateContext:
available_languages = ["en"]
dictionary: dict = {}
lang_per_session = {}
def get_available_languages():
return TranslateContext.available_languages
def set_available_languages(langs: list):
if not langs or not isinstance(langs, list):
raise ValueError("langs must be a list of languages")
TranslateContext.available_languages = langs
def get_default_language():
return TranslateContext.get_available_languages()[0]
def add_translation(translation: dict):
for k, v in translation.items():
if k not in TranslateContext.available_languages:
continue
if k not in TranslateContext.dictionary:
TranslateContext.dictionary[k] = {}
TranslateContext.dictionary[k].update(v)
def get_current_language(request: gr.Request):
return TranslateContext.lang_per_session.get(
request.session_hash, TranslateContext.get_default_language()
)
def set_current_language(request: gr.Request, lang: str):
TranslateContext.lang_per_session[request.session_hash] = lang
def get_lang_from_request(request: gr.Request):
if "Accept-Language" not in request.headers:
return TranslateContext.get_default_language()
# Get the first language from the Accept-Language header
lang = request.headers["Accept-Language"].split(",")[0]
lang, _ = langcodes.closest_match(
lang, TranslateContext.get_available_languages()
)
if not lang or lang == "und":
return TranslateContext.get_default_language()
return lang
class I18nString(str):
__slots__ = ("_key", "add_values", "radd_values")
def __new__(cls, value):
obj = super().__new__(cls, value)
obj._key = value
obj.add_values = []
obj.radd_values = []
return obj
def __str__(self):
try:
request = LocalContext.request.get()
except LookupError:
request = None
if request is None:
return self._key
lang = TranslateContext.get_current_language(request)
result = TranslateContext.dictionary.get(lang, {}).get(self._key, self._key)
for v in self.radd_values:
result = str(v) + result
for v in self.add_values:
result = result + str(v)
while len(result) >= 2 and result.startswith("'") and result.endswith("'"):
result = result[1:-1]
return result
def __add__(self, other):
self.add_values.append(other)
return self
def __radd__(self, other):
self.radd_values.append(other)
return self
def __hash__(self) -> int:
return super().__hash__()
def format(self, *args, **kwargs) -> str:
v = str(self)
if isinstance(v, I18nString):
return super().format(*args, **kwargs)
return v.format(*args, **kwargs)
def unwrap(self):
return super().__str__()
@staticmethod
def unwrap_strings(obj):
"""Unwrap all keys in I18nStrings in the object"""
if isinstance(obj, I18nString):
yield obj.unwrap()
for v in obj.add_values:
yield from I18nString.unwrap_strings(v)
for v in obj.radd_values:
yield from I18nString.unwrap_strings(v)
return
yield obj
def gettext(key: str):
"""Wrapper text string to return I18nString
:param key: The key of the I18nString
"""
return I18nString(key)
def iter_i18n_choices(choices):
"""Iterate all I18nStrings in the choice, returns the indices of the I18nStrings"""
if not isinstance(choices, list) or len(choices) == 0:
return
if isinstance(choices[0], tuple):
for i, (k, v) in enumerate(choices):
if isinstance(k, I18nString):
yield i
else:
for i, v in enumerate(choices):
if isinstance(v, I18nString):
yield i
def iter_i18n_fields(component: gr.components.Component):
"""Iterate all I18nStrings in the component"""
for name, value in inspect.getmembers(component):
if name == "value" and hasattr(component, "choices"):
# for those components with choices, the value will be kept as is
continue
if isinstance(value, I18nString):
yield name
elif name == "choices" and any(iter_i18n_choices(value)):
yield name
def iter_i18n_components(block: Block):
"""Iterate all I18nStrings in the block"""
if isinstance(block, BlockContext):
for component in block.children:
for c in iter_i18n_components(component):
yield c
if any(iter_i18n_fields(block)):
yield block
def has_new_i18n_fields(block: Block, existing_translation={}):
"""Check if there are new I18nStrings in the block
:param block: The block to check
:param existing_translation: The existing translation dictionary
:return: True if there are new I18nStrings, False otherwise
"""
components = list(iter_i18n_components(block))
for lang in TranslateContext.get_available_languages():
for component in components:
for field in iter_i18n_fields(component):
if field == "choices":
for idx in iter_i18n_choices(component.choices):
if isinstance(component.choices[idx], tuple):
value = component.choices[idx][0]
else:
value = component.choices[idx]
if value not in existing_translation.get(lang, {}):
return True
else:
value = getattr(component, field)
if value not in existing_translation.get(lang, {}):
return True
return False
def dump_blocks(block: Block, include_translations={}):
"""Dump all I18nStrings in the block to a dictionary
:param block: The block to dump
:param include_translations: The existing translation dictionary
:return: The dumped dictionary
"""
components = list(iter_i18n_components(block))
def translate(lang, key):
return include_translations.get(lang, {}).get(key, key)
ret = {}
for lang in TranslateContext.get_available_languages():
ret[lang] = {}
for component in components:
for field in iter_i18n_fields(component):
if field == "choices":
for idx in iter_i18n_choices(component.choices):
if isinstance(component.choices[idx], tuple):
value = component.choices[idx][0]
else:
value = component.choices[idx]
for key in I18nString.unwrap_strings(value):
ret[lang][key] = translate(lang, key)
else:
value = getattr(component, field)
for key in I18nString.unwrap_strings(value):
ret[lang][key] = translate(lang, key)
return ret
def translate_blocks(
block: gr.Blocks = None,
translation={},
lang: gr.components.Component = None,
persistant=False,
):
"""Translate all I18nStrings in the block
:param block: The block to translate, default is the root block
:param translation: The translation dictionary
:param lang: The language component to change the language
:param persistant: Whether to persist the language
"""
if block is None:
block = Context.root_block
"""Translate all I18nStrings in the block"""
if not isinstance(block, gr.Blocks):
raise ValueError("block must be an instance of gradio.Blocks")
components = list(iter_i18n_components(block))
TranslateContext.add_translation(translation)
hidden = gr.HTML(
value="""<style>
gradio-app {
visibility: hidden;
}
</style>"""
)
if persistant:
try:
from gradio import BrowserState
except ImportError:
raise ValueError("gradio>=5.6.0 is required for persistant language")
def on_lang_change(request: gr.Request, lang: str, saved_lang: str):
if not lang:
if saved_lang:
lang = saved_lang
else:
lang = TranslateContext.get_lang_from_request(request)
outputs = [lang, lang, ""]
TranslateContext.set_current_language(request, lang)
for component in components:
fields = list(iter_i18n_fields(component))
if component == lang and "value" in fields:
raise ValueError("'lang' component can't has I18nStrings as value")
modified = {}
for field in fields:
if field == "choices":
choices = component.choices.copy()
for idx in iter_i18n_choices(choices):
if isinstance(choices[idx], tuple):
k, v = choices[idx]
# We don't need to translate the value
choices[idx] = (str(k), next(I18nString.unwrap_strings(v)))
else:
v = choices[idx]
choices[idx] = (str(v), next(I18nString.unwrap_strings(v)))
modified[field] = choices
else:
modified[field] = str(getattr(component, field))
new_comp = gr.update(**modified)
outputs.append(new_comp)
if len(outputs) == 1:
return outputs[0]
return outputs
if lang is None:
lang = gr.State()
if persistant:
saved_lang = gr.BrowserState(storage_key="lang")
else:
saved_lang = gr.State()
gr.on(
[block.load, lang.change],
on_lang_change,
inputs=[lang, saved_lang],
outputs=[lang, saved_lang, hidden] + components,
)
@contextmanager
def Translate(
translation,
lang: gr.components.Component = None,
placeholder_langs=[],
persistant=False,
):
"""Translate all I18nStrings in the block
:param translation: The translation dictionary or file path
:param lang: The language component to change the language
:param placeholder_langs: The placeholder languages to create a new translation file if translation is a file path
:param persistant: Whether to persist the language
:return: The language component
"""
if lang is None:
lang = gr.State()
yield lang
if isinstance(translation, dict):
# Static translation
translation_dict = translation
pass
elif isinstance(translation, str):
if os.path.exists(translation):
# Regard as a file path
with open(translation, "r", encoding="utf-8") as f: # Force utf-8 encoding
if translation.endswith(".json"):
translation_dict = json.load(f)
elif translation.endswith(".yaml"):
translation_dict = yaml.safe_load(f)
else:
raise ValueError("Unsupported file format")
else:
translation_dict = {}
else:
raise ValueError("Unsupported translation type")
if placeholder_langs:
TranslateContext.set_available_languages(placeholder_langs)
block = Context.block
translate_blocks(
block=block, translation=translation_dict, lang=lang, persistant=persistant
)
if isinstance(translation, str) and has_new_i18n_fields(
block, existing_translation=translation_dict
):
merged = dump_blocks(block, include_translations=translation_dict)
with open(translation, "w") as f:
if translation.endswith(".json"):
json.dump(merged, f, indent=2, ensure_ascii=False)
elif translation.endswith(".yaml"):
yaml.dump(merged, f, allow_unicode=True, sort_keys=False)