File size: 4,610 Bytes
9c6594c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
from __future__ import annotations

import re
import warnings
from typing import Any


class I18nData:
    """
    A class that wraps a translation key with metadata.

    This object will be serialized and sent to the frontend, where the actual
    translation will happen using the frontend's i18n system.
    """

    def __init__(self, key: str):
        """
        Initialize a I18nData object.

        Args:
            key: The translation key to be translated in the frontend.
        """
        self.key = key
        self._type = "translation_metadata"

    def to_dict(self) -> dict[str, Any]:
        """
        Convert the I18nData object to a dictionary for serialization.
        This allows the frontend to recognize it as a translatable object.
        """
        return {"__type__": self._type, "key": self.key}

    def __str__(self) -> str:
        """
        String representation of the I18nData object.
        Used when the object is converted to a string.
        This returns a special format that can be recognized by the frontend
        as needing translation.
        """
        import json

        return f"__i18n__{json.dumps(self.to_dict())}"

    def __repr__(self) -> str:
        """
        Representation of the I18nData object for debugging.
        """
        return self.__str__()

    def __add__(self, other):
        """
        Handle string concatenation (self + other).
        """
        return str(self) + str(other)

    def __radd__(self, other):
        """
        Handle string concatenation (other + self).
        """
        return str(other) + str(self)

    def __getattr__(self, name):
        """
        Handle attribute access for I18nData.
        This makes it possible to use I18nData objects in contexts
        that expect strings with methods.
        """
        if name.startswith("__") and name.endswith("__"):
            raise AttributeError(f"{self.__class__.__name__} has no attribute {name}")

        def method(*_args, **_kwargs):
            return self

        return method

    def tojson(self) -> dict[str, Any]:
        """
        Convert the I18nData object to a JSON-serializable dictionary.
        This is used by the default Python JSON serializer.
        """
        return self.to_dict()


class I18n:
    """
    Handles internationalization (i18n) for Gradio applications.

    Stores translation dictionaries and provides a method to retrieve translation keys.
    The translation lookup happens on the frontend based on the browser's locale
    and the provided translation dictionaries.
    """

    # BCP 47 language tag regex pattern
    _LOCALE_PATTERN = re.compile(r"^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$")

    def __init__(self, **translations: dict[str, str]):
        """
        Initializes the I18n class.

        Args:
            **translations: Each keyword argument should be a locale code (e.g., "en", "fr") with a
                          dictionary value, which maps translation keys to translated strings.
                          Example: gr.I18n(en={"greeting": "Hello"}, es={"greeting": "Hola"})

                          These translations can be passed to the frontend for use there.
        """
        self.translations = {}

        for locale, translation_dict in translations.items():
            if not self._is_valid_locale(locale):
                warnings.warn(
                    f"Invalid locale code: '{locale}'. Locale codes should follow BCP 47 format (e.g., 'en', 'en-US'). "
                    f"This locale will still be included, but may not work correctly.",
                    UserWarning,
                )
            self.translations[locale] = translation_dict

    def _is_valid_locale(self, locale: str) -> bool:
        return bool(self._LOCALE_PATTERN.match(locale))

    def __call__(self, key: str) -> I18nData:
        """
        Returns a I18nData object containing the translation key.

        This metadata object will be serialized and sent to the frontend,
        where it will be translated by the frontend's i18n system.

        Args:
            key: The key to identify the translation string (e.g., "submit_button").

        Returns:
            A I18nData object containing the translation key.
        """
        return I18nData(key)

    @property
    def translations_dict(self) -> dict[str, dict[str, str]]:
        """
        Returns the dictionary of translations provided during initialization.
        These can be passed to the frontend for use in its translation system.
        """
        return self.translations