Spaces:
Sleeping
Sleeping
# 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): | |
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__() | |
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, | |
) | |
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) | |