Repository: MyreMylar/pygame_gui (python)

Issue:
MessageWindow - variables in translated strings
**Is your feature request related to a problem? Please describe.**
I want to be able to use variables in translated strings, so I could let the user know about things like which file they're about to overwrite.

**Describe the solution you'd like**
The solution described in this discussion would be fine: https://github.com/MyreMylar/pygame_gui/discussions/261

**Additional context**
Labels already support variables, but MessageWindows and other elements don't.


Pull request:
Add translation text key word argument dictionary params
Allows most elements with text to accept a dictionary of keyword arguments that are passed to the translation engine for insertion into translated strings.

A keyword argument dictionary of string to strings was chosen over raw keyword arguments as it lets us be more flexible in adding arguments to elements in the future.

fixes #270

Usage Example:

```
message_window = UIMessageWindow(rect=pygame.Rect(100, 100, 250, 300),
                                         window_title="Test Message",
                                         html_message="app_translations.test_hello",
                                         manager=default_ui_manager,
                                         html_message_text_kwargs={"name": "World"})

```

Example `app_translation.en.json` file:

```
{
  "en": {
    "test_hello": "Hello %{name}"
  }
}
```


Patch:
diff --git a/pygame_gui/core/interfaces/manager_interface.py b/pygame_gui/core/interfaces/manager_interface.py
index 17f12d1..6dd8246 100644
--- a/pygame_gui/core/interfaces/manager_interface.py
+++ b/pygame_gui/core/interfaces/manager_interface.py
@@ -1,5 +1,5 @@
 from abc import ABCMeta, abstractmethod
-from typing import Tuple, List, Union, Dict, Set
+from typing import Tuple, List, Union, Dict, Set, Optional
 
 import pygame
 
@@ -226,13 +226,18 @@ class IUIManagerInterface(metaclass=ABCMeta):
     def create_tool_tip(self,
                         text: str,
                         position: Tuple[int, int],
-                        hover_distance: Tuple[int, int]) -> IUITooltipInterface:
+                        hover_distance: Tuple[int, int],
+                        *,
+                        text_kwargs: Optional[Dict[str, str]] = None) -> IUITooltipInterface:
         """
         Creates a tool tip ands returns it.
 
         :param text: The tool tips text, can utilise the HTML subset used in all UITextBoxes.
         :param position: The screen position to create the tool tip for.
         :param hover_distance: The distance we should hover away from our target position.
+        :param text_kwargs: a dictionary of variable arguments to pass to the translated string
+                            useful when you have multiple translations that need variables inserted
+                            in the middle.
 
         :return: A tool tip placed somewhere on the screen.
 
diff --git a/pygame_gui/core/text/text_box_layout_row.py b/pygame_gui/core/text/text_box_layout_row.py
index eb3f509..20d29b3 100644
--- a/pygame_gui/core/text/text_box_layout_row.py
+++ b/pygame_gui/core/text/text_box_layout_row.py
@@ -18,7 +18,7 @@ class TextBoxLayoutRow(pygame.Rect):
         self.line_spacing = line_spacing
         self.row_index = row_index
         self.layout = layout
-        self.items = []
+        self.items: TextLayoutRect = []
 
         self.letter_count = 0
 
diff --git a/pygame_gui/elements/ui_button.py b/pygame_gui/elements/ui_button.py
index 6e88564..50e9a1b 100644
--- a/pygame_gui/elements/ui_button.py
+++ b/pygame_gui/elements/ui_button.py
@@ -41,6 +41,9 @@ class UIButton(UIElement):
                                 unique event.
     :param visible: Whether the element is visible by default. Warning - container visibility may
                     override this.
+    :param text_kwargs: a dictionary of variable arguments to pass to the translated string
+                        useful when you have multiple translations that need variables inserted
+                        in the middle.
     """
 
     def __init__(self, relative_rect: Union[pygame.Rect, Tuple[float, float], pygame.Vector2],
@@ -54,7 +57,10 @@ class UIButton(UIElement):
                  anchors: Dict[str, Union[str, UIElement]] = None,
                  allow_double_clicks: bool = False,
                  generate_click_events_from: Iterable[int] = frozenset([pygame.BUTTON_LEFT]),
-                 visible: int = 1
+                 visible: int = 1,
+                 *,
+                 text_kwargs: Optional[Dict[str, str]] = None,
+                 tool_tip_text_kwargs: Optional[Dict[str, str]] = None,
                  ):
 
         rel_rect = (relative_rect if isinstance(relative_rect, pygame.Rect)
@@ -72,12 +78,18 @@ class UIButton(UIElement):
                                element_id='button')
 
         self.text = text
+        self.text_kwargs = {}
+        if text_kwargs is not None:
+            self.text_kwargs = text_kwargs
 
         self.dynamic_width = False
         self.dynamic_height = False
         self.dynamic_dimensions_orig_top_left = rel_rect.topleft
         # support for an optional 'tool tip' element attached to this button
         self.tool_tip_text = tool_tip_text
+        self.tool_tip_text_kwargs = {}
+        if tool_tip_text_kwargs is not None:
+            self.tool_tip_text_kwargs = tool_tip_text_kwargs
         self.tool_tip = None
         self.ui_root_container = self.ui_manager.get_root_container()
 
@@ -259,7 +271,8 @@ class UIButton(UIElement):
                                                             position=(mouse_pos[0],
                                                                       self.rect.centery),
                                                             hover_distance=(0,
-                                                                            hover_height))
+                                                                            hover_height),
+                                                            text_kwargs=self.tool_tip_text_kwargs)
 
         self.hover_time += time_delta
 
@@ -445,19 +458,34 @@ class UIButton(UIElement):
         self.is_selected = False
         self.drawable_shape.set_active_state('normal')
 
-    def set_text(self, text: str):
+    def set_text(self, text: str, *, text_kwargs: Optional[Dict[str, str]] = None):
         """
         Sets the text on the button. The button will rebuild.
 
         :param text: The new text to set.
+        :param text_kwargs: a dictionary of variable arguments to pass to the translated string
+                        useful when you have multiple translations that need variables inserted
+                        in the middle.
 
         """
+        any_changed = False
         if text != self.text:
             self.text = text
+            any_changed = True
+
+        if text_kwargs is not None:
+            if text_kwargs != self.text_kwargs:
+                self.text_kwargs = text_kwargs
+                any_changed = True
+        elif self.text_kwargs != {}:
+            self.text_kwargs = {}
+            any_changed = True
+
+        if any_changed:
             if self.dynamic_width:
                 self.rebuild()
             else:
-                self.drawable_shape.set_text(translate(self.text))
+                self.drawable_shape.set_text(translate(self.text, **self.text_kwargs))
 
     def set_hold_range(self, xy_range: Tuple[int, int]):
         """
@@ -694,7 +722,7 @@ class UIButton(UIElement):
                               'border_width': self.border_width,
                               'shadow_width': self.shadow_width,
                               'font': self.font,
-                              'text': translate(self.text),
+                              'text': translate(self.text, **self.text_kwargs),
                               'text_shadow': (self.text_shadow_size,
                                               self.text_shadow_offset[0],
                                               self.text_shadow_offset[1],
@@ -761,4 +789,4 @@ class UIButton(UIElement):
             if self.dynamic_width:
                 self.rebuild()
             else:
-                self.drawable_shape.set_text(translate(self.text))
+                self.drawable_shape.set_text(translate(self.text, **self.text_kwargs))
diff --git a/pygame_gui/elements/ui_label.py b/pygame_gui/elements/ui_label.py
index 474a047..9a95e0f 100644
--- a/pygame_gui/elements/ui_label.py
+++ b/pygame_gui/elements/ui_label.py
@@ -33,6 +33,9 @@ class UILabel(UIElement, IUITextOwnerInterface):
     :param anchors: A dictionary describing what this element's relative_rect is relative to.
     :param visible: Whether the element is visible by default. Warning - container visibility
                     may override this.
+    :param text_kwargs: a dictionary of variable arguments to pass to the translated string
+                        useful when you have multiple translations that need variables inserted
+                        in the middle.
     """
 
     def __init__(self, relative_rect: pygame.Rect,
@@ -44,7 +47,7 @@ class UILabel(UIElement, IUITextOwnerInterface):
                  anchors: Optional[Dict[str, Union[str, UIElement]]] = None,
                  visible: int = 1,
                  *,
-                 text_kwargs: Dict[str, str] = None):
+                 text_kwargs: Optional[Dict[str, str]] = None):
 
         super().__init__(relative_rect, manager, container,
                          starting_height=1,
@@ -86,19 +89,26 @@ class UILabel(UIElement, IUITextOwnerInterface):
 
         self.rebuild_from_changed_theme_data()
 
-    def set_text(self, text: str, **text_kwargs: str):
+    def set_text(self, text: str, *, text_kwargs: Optional[Dict[str, str]] = None):
         """
         Changes the string displayed by the label element. Labels do not support HTML styling.
 
         :param text: the text to set the label to.
+        :param text_kwargs: a dictionary of variable arguments to pass to the translated string
+                            useful when you have multiple translations that need variables inserted
+                            in the middle.
 
         """
         any_changed = False
         if text != self.text:
             self.text = text
             any_changed = True
-        if text_kwargs != self.text_kwargs:
-            self.text_kwargs = text_kwargs
+        if text_kwargs is not None:
+            if text_kwargs != self.text_kwargs:
+                self.text_kwargs = text_kwargs
+                any_changed = True
+        elif self.text_kwargs != {}:
+            self.text_kwargs = {}
             any_changed = True
 
         if any_changed:
diff --git a/pygame_gui/elements/ui_text_box.py b/pygame_gui/elements/ui_text_box.py
index b7673c2..211fb90 100644
--- a/pygame_gui/elements/ui_text_box.py
+++ b/pygame_gui/elements/ui_text_box.py
@@ -67,6 +67,9 @@ class UITextBox(UIElement, IUITextOwnerInterface):
     :param anchors: A dictionary describing what this element's relative_rect is relative to.
     :param visible: Whether the element is visible by default. Warning - container visibility
                     may override this.
+    :param text_kwargs: a dictionary of variable arguments to pass to the translated text
+                        useful when you have multiple translations that need variables inserted
+                        in the middle.
     """
 
     def __init__(self,
@@ -81,7 +84,8 @@ class UITextBox(UIElement, IUITextOwnerInterface):
                  anchors: Optional[Dict[str, Union[str, UIElement]]] = None,
                  visible: int = 1,
                  *,
-                 pre_parsing_enabled: bool = True):
+                 pre_parsing_enabled: bool = True,
+                 text_kwargs: Optional[Dict[str, str]] = None):
 
         super().__init__(relative_rect, manager, container,
                          starting_height=layer_starting_height,
@@ -97,6 +101,9 @@ class UITextBox(UIElement, IUITextOwnerInterface):
 
         self.html_text = html_text
         self.appended_text = ""
+        self.text_kwargs = {}
+        if text_kwargs is not None:
+            self.text_kwargs = text_kwargs
         self.font_dict = self.ui_theme.get_font_dictionary()
 
         self._pre_parsing_enabled = pre_parsing_enabled
@@ -519,7 +526,7 @@ class UITextBox(UIElement, IUITextOwnerInterface):
         rendered text.
         """
 
-        self.parser.feed(self._pre_parse_text(translate(self.html_text) + self.appended_text))
+        self.parser.feed(self._pre_parse_text(translate(self.html_text, **self.text_kwargs) + self.appended_text))
 
         self.text_box_layout = TextBoxLayout(self.parser.layout_rect_queue,
                                              pygame.Rect((0, 0), (self.text_wrap_rect[2],
@@ -1057,8 +1064,12 @@ class UITextBox(UIElement, IUITextOwnerInterface):
     def get_object_id(self) -> str:
         return self.most_specific_combined_id
 
-    def set_text(self, html_text: str):
+    def set_text(self, html_text: str, *, text_kwargs: Optional[Dict[str, str]] = None):
         self.html_text = html_text
+        if text_kwargs is not None:
+            self.text_kwargs = text_kwargs
+        else:
+            self.text_kwargs = {}
         self.appended_text = ""  # clear appended text as it feels odd to set the text and still have appended text
         self._reparse_and_rebuild()
 
diff --git a/pygame_gui/elements/ui_text_entry_line.py b/pygame_gui/elements/ui_text_entry_line.py
index 3759838..ae89cd7 100644
--- a/pygame_gui/elements/ui_text_entry_line.py
+++ b/pygame_gui/elements/ui_text_entry_line.py
@@ -89,7 +89,7 @@ class UITextEntryLine(UIElement):
 
         self.text = ""
         if initial_text is not None:
-            self.text = initial_text
+            self.text = translate(initial_text)
         self.is_text_hidden = False
         self.hidden_text_char = '●'
         self.placeholder_text = ""
@@ -216,7 +216,7 @@ class UITextEntryLine(UIElement):
                               'border_width': self.border_width,
                               'shadow_width': self.shadow_width,
                               'font': self.font,
-                              'text': display_text if len(display_text) > 0 else self.placeholder_text,
+                              'text': display_text if len(display_text) > 0 else translate(self.placeholder_text),
                               'text_width': -1,
                               'text_horiz_alignment': 'left',
                               'text_vert_alignment': 'centre',
@@ -276,7 +276,8 @@ class UITextEntryLine(UIElement):
                 if self.is_text_hidden:
                     display_text = self.hidden_text_char * len(self.text)
                 if self.drawable_shape is not None:
-                    self.drawable_shape.set_text(display_text if len(display_text) > 0 else self.placeholder_text)
+                    self.drawable_shape.set_text(display_text if len(display_text) > 0
+                                                 else translate(self.placeholder_text))
                     self.drawable_shape.text_box_layout.set_cursor_position(self.edit_position)
                     self.drawable_shape.apply_active_text_changes()
             else:
@@ -366,7 +367,7 @@ class UITextEntryLine(UIElement):
         self.text_entered = False
         if len(self.text) == 0:
             if self.drawable_shape is not None:
-                self.drawable_shape.set_text(self.placeholder_text)
+                self.drawable_shape.set_text(translate(self.placeholder_text))
                 self.drawable_shape.text_box_layout.set_cursor_position(self.edit_position)
                 self.drawable_shape.apply_active_text_changes()
         self.redraw()
@@ -997,7 +998,7 @@ class UITextEntryLine(UIElement):
 
     def disable(self):
         """
-        Disables the button so that it is no longer interactive.
+        Disables the element so that it is no longer interactive.
         """
         if self.is_enabled:
             self.is_enabled = False
@@ -1017,7 +1018,7 @@ class UITextEntryLine(UIElement):
 
     def enable(self):
         """
-        Re-enables the button so we can once again interact with it.
+        Re-enables the element so we can once again interact with it.
         """
         if not self.is_enabled:
             self.is_enabled = True
@@ -1033,8 +1034,12 @@ class UITextEntryLine(UIElement):
             self.font = font
             self.rebuild()
         else:
+            display_text = self.text
+            if self.is_text_hidden:
+                display_text = self.hidden_text_char * len(self.text)
             if self.drawable_shape is not None:
-                self.drawable_shape.set_text(translate(self.text))
+                self.drawable_shape.set_text(display_text if len(display_text) > 0
+                                             else translate(self.placeholder_text))
 
     def clear(self):
         self.set_text("")
diff --git a/pygame_gui/elements/ui_tool_tip.py b/pygame_gui/elements/ui_tool_tip.py
index 133a4c1..43efa2b 100644
--- a/pygame_gui/elements/ui_tool_tip.py
+++ b/pygame_gui/elements/ui_tool_tip.py
@@ -30,6 +30,9 @@ class UITooltip(UIElement, IUITooltipInterface):
     :param parent_element: The element this element 'belongs to' in the theming hierarchy.
     :param object_id: A custom defined ID for fine tuning of theming.
     :param anchors: A dictionary describing what this element's relative_rect is relative to.
+    :param text_kwargs: a dictionary of variable arguments to pass to the translated text
+                        useful when you have multiple translations that need variables inserted
+                        in the middle.
 
     """
     def __init__(self,
@@ -38,7 +41,9 @@ class UITooltip(UIElement, IUITooltipInterface):
                  manager: Optional[IUIManagerInterface] = None,
                  parent_element: Optional[UIElement] = None,
                  object_id: Optional[Union[ObjectID, str]] = None,
-                 anchors: Optional[Dict[str, Union[str, UIElement]]] = None):
+                 anchors: Optional[Dict[str, Union[str, UIElement]]] = None,
+                 *,
+                 text_kwargs: Dict[str, str] = None):
 
         super().__init__(relative_rect=pygame.Rect((0, 0), (-1, -1)),
                          manager=manager,
@@ -62,7 +67,8 @@ class UITooltip(UIElement, IUITooltipInterface):
                                     pygame.Rect(0, 0, self.rect_width, -1),
                                     manager=self.ui_manager,
                                     layer_starting_height=self._layer,
-                                    parent_element=self)
+                                    parent_element=self,
+                                    text_kwargs=text_kwargs)
 
         self.rect_width = self.text_block.rect.size[0]
         super().set_dimensions(self.text_block.rect.size)
diff --git a/pygame_gui/ui_manager.py b/pygame_gui/ui_manager.py
index 7f9acf7..52a1539 100644
--- a/pygame_gui/ui_manager.py
+++ b/pygame_gui/ui_manager.py
@@ -521,7 +521,9 @@ class UIManager(IUIManagerInterface):
         return self.universal_empty_surface
 
     def create_tool_tip(self, text: str, position: Tuple[int, int],
-                        hover_distance: Tuple[int, int]) -> IUITooltipInterface:
+                        hover_distance: Tuple[int, int],
+                        *,
+                        text_kwargs: Optional[Dict[str, str]] = None) -> IUITooltipInterface:
         """
         Creates a tool tip ands returns it. Have hidden this away in the manager so we can call it
         from other UI elements and create tool tips without creating cyclical import problems.
@@ -529,10 +531,13 @@ class UIManager(IUIManagerInterface):
         :param text: The tool tips text, can utilise the HTML subset used in all UITextBoxes.
         :param position: The screen position to create the tool tip for.
         :param hover_distance: The distance we should hover away from our target position.
+        :param text_kwargs: a dictionary of variable arguments to pass to the translated string
+                            useful when you have multiple translations that need variables inserted
+                            in the middle.
 
         :return: A tool tip placed somewhere on the screen.
         """
-        tool_tip = UITooltip(text, hover_distance, self)
+        tool_tip = UITooltip(text, hover_distance, self, text_kwargs=text_kwargs)
         tool_tip.find_valid_position(pygame.math.Vector2(position[0], position[1]))
         return tool_tip
 
diff --git a/pygame_gui/windows/ui_confirmation_dialog.py b/pygame_gui/windows/ui_confirmation_dialog.py
index 67c2edd..881ef35 100644
--- a/pygame_gui/windows/ui_confirmation_dialog.py
+++ b/pygame_gui/windows/ui_confirmation_dialog.py
@@ -1,5 +1,5 @@
 import warnings
-from typing import Union, Optional
+from typing import Union, Optional, Dict
 
 import pygame
 
@@ -28,6 +28,9 @@ class UIConfirmationDialog(UIWindow):
     :param object_id: A custom defined ID for fine tuning of theming. Defaults to
                       '#confirmation_dialog'.
     :param visible: Whether the element is visible by default.
+    :param long_desc_text_kwargs: a dictionary of variable arguments to pass to the translated string
+                                  useful when you have multiple translations that need variables inserted
+                                  in the middle.
     """
 
     def __init__(self, rect: pygame.Rect,
@@ -38,7 +41,9 @@ class UIConfirmationDialog(UIWindow):
                  action_short_name: str = 'pygame-gui.OK',
                  blocking: bool = True,
                  object_id: Union[ObjectID, str] = ObjectID('#confirmation_dialog', None),
-                 visible: int = 1):
+                 visible: int = 1,
+                 action_long_desc_text_kwargs: Optional[Dict[str, str]] = None
+                 ):
 
         super().__init__(rect, manager,
                          window_display_title=window_title,
@@ -86,7 +91,8 @@ class UIConfirmationDialog(UIWindow):
                                            anchors={'left': 'left',
                                                     'right': 'right',
                                                     'top': 'top',
-                                                    'bottom': 'bottom'})
+                                                    'bottom': 'bottom'},
+                                           text_kwargs=action_long_desc_text_kwargs)
 
         self.set_blocking(blocking)
 
diff --git a/pygame_gui/windows/ui_message_window.py b/pygame_gui/windows/ui_message_window.py
index 5895e0b..e73ea78 100644
--- a/pygame_gui/windows/ui_message_window.py
+++ b/pygame_gui/windows/ui_message_window.py
@@ -1,5 +1,5 @@
 import warnings
-from typing import Union, Optional
+from typing import Union, Optional, Dict
 
 import pygame
 
@@ -27,7 +27,8 @@ class UIMessageWindow(UIWindow):
                  *,
                  window_title: str = 'pygame-gui.message_window_title_bar',
                  object_id: Union[ObjectID, str] = ObjectID('#message_window', None),
-                 visible: int = 1):
+                 visible: int = 1,
+                 html_message_text_kwargs: Optional[Dict[str, str]] = None):
 
         super().__init__(rect, manager,
                          window_display_title=window_title,
@@ -71,8 +72,8 @@ class UIMessageWindow(UIWindow):
                                     anchors={"left": "left",
                                              "top": "top",
                                              "right": "right",
-                                             "bottom": "bottom"}
-                                    )
+                                             "bottom": "bottom"},
+                                    text_kwargs=html_message_text_kwargs)
 
     def process_event(self, event: pygame.event.Event) -> bool:
         """


Test patch:
diff --git a/tests/test_elements/test_ui_button.py b/tests/test_elements/test_ui_button.py
index 6afab1b..c1ca3d1 100644
--- a/tests/test_elements/test_ui_button.py
+++ b/tests/test_elements/test_ui_button.py
@@ -4,6 +4,8 @@ import pytest
 import pygame
 import pygame_gui
 
+import i18n
+
 from tests.shared_comparators import compare_surfaces
 
 from pygame_gui.ui_manager import UIManager
@@ -21,6 +23,35 @@ class TestUIButton:
                           tool_tip_text="This is a test of the button's tool tip functionality.")
         assert button.image is not None
 
+        i18n.add_translation('translation.test_hello', 'Hello %{name}')
+        button_with_kwargs = UIButton(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                      text="translation.test_hello",
+                                      manager=default_ui_manager,
+                                      text_kwargs={"name": "World"})
+        assert button_with_kwargs.image is not None
+        assert button_with_kwargs.drawable_shape.theming['text'] == "Hello World"
+
+        button_with_tt_kwargs = UIButton(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                         text="Test Button",
+                                         manager=default_ui_manager,
+                                         tool_tip_text="translation.test_hello",
+                                         tool_tip_text_kwargs={"name": "World"})
+        assert button_with_tt_kwargs.image is not None
+
+    def test_kwargs_set_text(self, _init_pygame, default_ui_manager,
+                             _display_surface_return_none):
+        i18n.add_translation('translation.test_hello', 'Hello %{name}')
+        button = UIButton(relative_rect=pygame.Rect(100, 100, 150, 30),
+                          text="Test Button",
+                          manager=default_ui_manager)
+        button.set_text("translation.test_hello", text_kwargs={"name": "World"})
+        assert button.image is not None
+        assert button.drawable_shape.theming['text'] == "Hello World"
+
+        button.set_text("Clear args")
+        assert button.image is not None
+        assert button.drawable_shape.theming['text'] == "Clear args"
+
     @pytest.mark.filterwarnings("ignore:DeprecationWarning")
     def test_set_any_images_from_theme(self, _init_pygame, _display_surface_return_none):
         manager = UIManager((800, 600),
diff --git a/tests/test_elements/test_ui_label.py b/tests/test_elements/test_ui_label.py
index c7dcfa0..f13aafb 100644
--- a/tests/test_elements/test_ui_label.py
+++ b/tests/test_elements/test_ui_label.py
@@ -2,6 +2,8 @@ import os
 import pytest
 import pygame
 
+import i18n
+
 from tests.shared_comparators import compare_surfaces
 
 from pygame_gui.ui_manager import UIManager
@@ -18,11 +20,13 @@ class TestUILabel:
                         manager=default_ui_manager)
         assert label.image is not None
 
+        i18n.add_translation('translation.test_hello', 'Hello %{name}')
         label_with_kwargs = UILabel(relative_rect=pygame.Rect(100, 100, 150, 30),
-                                    text="Hello %{name}",
+                                    text="translation.test_hello",
                                     manager=default_ui_manager,
-                                    text_kwargs={"name": "Dan"})
+                                    text_kwargs={"name": "World"})
         assert label_with_kwargs.image is not None
+        assert label_with_kwargs.drawable_shape.theming['text'] == "Hello World"
 
     def test_set_text(self, _init_pygame, default_ui_manager,
                       _display_surface_return_none):
@@ -41,11 +45,17 @@ class TestUILabel:
 
     def test_kwargs_set_text(self, _init_pygame, default_ui_manager,
                              _display_surface_return_none):
+        i18n.add_translation('translation.test_hello', 'Hello %{name}')
         label = UILabel(relative_rect=pygame.Rect(100, 100, 150, 30),
                         text="Test Label",
                         manager=default_ui_manager)
-        label.set_text("Hello %{name}", name="Dan")
+        label.set_text("translation.test_hello", text_kwargs={"name": "World"})
+        assert label.image is not None
+        assert label.drawable_shape.theming['text'] == "Hello World"
+
+        label.set_text("Clear args")
         assert label.image is not None
+        assert label.drawable_shape.theming['text'] == "Clear args"
 
     def test_rebuild(self, _init_pygame, default_ui_manager,
                      _display_surface_return_none):
diff --git a/tests/test_elements/test_ui_text_box.py b/tests/test_elements/test_ui_text_box.py
index 1e6c1ed..a7a07b5 100644
--- a/tests/test_elements/test_ui_text_box.py
+++ b/tests/test_elements/test_ui_text_box.py
@@ -3,10 +3,13 @@ import os
 import pygame
 import pytest
 
+import i18n
+
 import pygame_gui
 from pygame_gui.elements.ui_text_box import UITextBox
 from pygame_gui.ui_manager import UIManager
 from tests.shared_comparators import compare_surfaces
+from pygame_gui.core.text.text_line_chunk import TextLineChunkFTFont
 
 from pygame_gui import UITextEffectType
 
@@ -23,6 +26,15 @@ class TestUITextBox:
                              relative_rect=pygame.Rect(100, 100, 200, 300),
                              manager=default_ui_manager)
         assert text_box.image is not None
+        i18n.add_translation('translation.test_hello', 'Hello %{name}')
+        text_box_with_kwargs = UITextBox(relative_rect=pygame.Rect(100, 100, 150, 30),
+                                         html_text="translation.test_hello",
+                                         manager=default_ui_manager,
+                                         text_kwargs={"name": "World"})
+        assert text_box_with_kwargs.image is not None
+        text_chunk = text_box_with_kwargs.text_box_layout.layout_rows[0].items[0]
+        assert isinstance(text_chunk, TextLineChunkFTFont)
+        assert text_chunk.text == "Hello World"
 
     def test_set_text(self, _init_pygame: None,
                       default_ui_manager: UIManager,
@@ -38,6 +50,21 @@ class TestUITextBox:
         text_box.set_text("<b>Changed text</b>")
         assert text_box.image is not None
 
+    def test_kwargs_set_text(self, _init_pygame, default_ui_manager,
+                             _display_surface_return_none):
+        i18n.add_translation('translation.test_hello', 'Hello %{name}')
+        default_ui_manager.preload_fonts([{"name": "fira_code", "size:": 14, "style": "bold"},
+                                          {"name": "fira_code", "size:": 14, "style": "italic"}])
+        text_box = UITextBox(html_text="<font color=#FF0000>Some text</font> in a <b>bold box</b> using colours and "
+                                       "<i>styles</i>.",
+                             relative_rect=pygame.Rect(100, 100, 200, 300),
+                             manager=default_ui_manager)
+        text_box.set_text("translation.test_hello", text_kwargs={"name": "World"})
+        assert text_box.image is not None
+        text_chunk = text_box.text_box_layout.layout_rows[0].items[0]
+        assert isinstance(text_chunk, TextLineChunkFTFont)
+        assert text_chunk.text == "Hello World"
+
     def test_clear(self, _init_pygame: None,
                    default_ui_manager: UIManager,
                    _display_surface_return_none):
diff --git a/tests/test_elements/test_ui_text_entry_line.py b/tests/test_elements/test_ui_text_entry_line.py
index 8a0d6f5..21ef484 100644
--- a/tests/test_elements/test_ui_text_entry_line.py
+++ b/tests/test_elements/test_ui_text_entry_line.py
@@ -3,6 +3,8 @@ import pytest
 import pygame
 import platform
 
+import i18n
+
 from tests.shared_comparators import compare_surfaces
 
 from pygame_gui.ui_manager import UIManager
@@ -20,6 +22,7 @@ class TestUITextEntryLine:
         assert text_entry.image is not None
 
     def test_placeholder_text(self, _init_pygame, default_ui_manager):
+
         text_entry = UITextEntryLine(relative_rect=pygame.Rect(100, 100, 200, 30),
                                      manager=default_ui_manager,
                                      placeholder_text="Enter name...")
@@ -42,6 +45,14 @@ class TestUITextEntryLine:
 
         assert text_entry.drawable_shape.theming['text'] == "Enter name..."
 
+        i18n.add_translation('translation.enter_age', 'Enter age...')
+
+        text_entry = UITextEntryLine(relative_rect=pygame.Rect(100, 100, 200, 30),
+                                     manager=default_ui_manager,
+                                     placeholder_text='translation.enter_age')
+        assert text_entry.image is not None
+        assert text_entry.drawable_shape.theming['text'] == "Enter age..."
+
     def test_initial_text(self, _init_pygame, default_ui_manager):
         text_entry = UITextEntryLine(relative_rect=pygame.Rect(100, 100, 200, 30),
                                      manager=default_ui_manager,
@@ -1148,6 +1159,13 @@ class TestUITextEntryLine:
 
         default_ui_manager.set_locale('ja')
 
+        default_ui_manager.set_locale('en')
+        text_entry.set_text_hidden(True)
+
+        default_ui_manager.set_locale('fr')
+
+        text_entry.set_text_hidden(False)
+
         assert text_entry.get_text() == "Some basic text"
 
 
diff --git a/tests/test_elements/test_ui_tool_tip.py b/tests/test_elements/test_ui_tool_tip.py
index f88530f..920530f 100644
--- a/tests/test_elements/test_ui_tool_tip.py
+++ b/tests/test_elements/test_ui_tool_tip.py
@@ -2,8 +2,11 @@ import os
 import pytest
 import pygame
 
+import i18n
+
 from pygame_gui.ui_manager import UIManager
 from pygame_gui.elements.ui_tool_tip import UITooltip
+from pygame_gui.core.text.text_line_chunk import TextLineChunkFTFont
 
 
 class TestUIToolTip:
@@ -14,6 +17,16 @@ class TestUIToolTip:
                              manager=default_ui_manager)
         assert tool_tip.text_block.image is not None
 
+        i18n.add_translation('translation.test_hello', 'Hello %{name}')
+        tool_tip = UITooltip(html_text="translation.test_hello",
+                             hover_distance=(0, 10),
+                             manager=default_ui_manager,
+                             text_kwargs={"name": "World"})
+        assert tool_tip.text_block.image is not None
+        text_chunk = tool_tip.text_block.text_box_layout.layout_rows[0].items[0]
+        assert isinstance(text_chunk, TextLineChunkFTFont)
+        assert text_chunk.text == "Hello World"
+
     def test_rebuild(self, _init_pygame, default_ui_manager, _display_surface_return_none):
         tool_tip = UITooltip(html_text="A tip about tools.",
                              hover_distance=(0, 10),
diff --git a/tests/test_windows/test_ui_confirmation_dialog.py b/tests/test_windows/test_ui_confirmation_dialog.py
index 045953b..637b9dc 100644
--- a/tests/test_windows/test_ui_confirmation_dialog.py
+++ b/tests/test_windows/test_ui_confirmation_dialog.py
@@ -3,10 +3,13 @@ import pygame
 import pytest
 import pygame_gui
 
+import i18n
+
 from tests.shared_comparators import compare_surfaces
 
 from pygame_gui.ui_manager import UIManager
 from pygame_gui.windows import UIConfirmationDialog
+from pygame_gui.core.text.text_line_chunk import TextLineChunkFTFont
 
 
 class TestUIConfirmationDialog:
@@ -21,6 +24,19 @@ class TestUIConfirmationDialog:
                              window_title="Confirm",
                              action_short_name="Confirm")
 
+        i18n.add_translation('translation.test_hello', 'Hello %{name}')
+
+        confirmation_dialog = UIConfirmationDialog(rect=pygame.Rect(100, 100, 400, 300),
+                                                   action_long_desc="translation.test_hello",
+                                                   manager=default_ui_manager,
+                                                   window_title="Confirm",
+                                                   action_short_name="Confirm",
+                                                   action_long_desc_text_kwargs={"name": "World"})
+        assert confirmation_dialog.confirmation_text.image is not None
+        text_chunk = confirmation_dialog.confirmation_text.text_box_layout.layout_rows[0].items[0]
+        assert isinstance(text_chunk, TextLineChunkFTFont)
+        assert text_chunk.text == "Hello World"
+
     def test_create_too_small(self, _init_pygame, default_ui_manager,
                               _display_surface_return_none):
         default_ui_manager.preload_fonts([{'name': 'fira_code', 'point_size': 14, 'style': 'bold'}])
diff --git a/tests/test_windows/test_ui_message_window.py b/tests/test_windows/test_ui_message_window.py
index b19d55c..8608d3b 100644
--- a/tests/test_windows/test_ui_message_window.py
+++ b/tests/test_windows/test_ui_message_window.py
@@ -2,11 +2,13 @@ import os
 import pygame
 import pytest
 
+import i18n
+
 from tests.shared_comparators import compare_surfaces
 
 from pygame_gui.ui_manager import UIManager
 from pygame_gui.windows.ui_message_window import UIMessageWindow
-
+from pygame_gui.core.text.text_line_chunk import TextLineChunkFTFont
 
 
 class TestUIMessageWindow:
@@ -16,12 +18,25 @@ class TestUIMessageWindow:
         default_ui_manager.preload_fonts([{'name': 'fira_code',
                                            'point_size': 14,
                                            'style': 'bold'}])
+
         UIMessageWindow(rect=pygame.Rect(100, 100, 250, 300),
                         window_title="Test Message",
                         html_message="This is a <b>bold</b> test "
                                      "of the message box functionality.",
                         manager=default_ui_manager)
 
+        i18n.add_translation('translation.test_hello', 'Hello %{name}')
+        message_window = UIMessageWindow(rect=pygame.Rect(100, 100, 250, 300),
+                                         window_title="Test Message",
+                                         html_message="translation.test_hello",
+                                         manager=default_ui_manager,
+                                         html_message_text_kwargs={"name": "World"})
+
+        assert message_window.text_block.image is not None
+        text_chunk = message_window.text_block.text_box_layout.layout_rows[0].items[0]
+        assert isinstance(text_chunk, TextLineChunkFTFont)
+        assert text_chunk.text == "Hello World"
+
     def test_create_too_small(self, _init_pygame, default_ui_manager,
                               _display_surface_return_none):
         default_ui_manager.preload_fonts([{'name': 'fira_code',