Internationalisation

switch UI language and watch plural rules update live.

▶ Run in browser

Tags: ui i18n localisation text plural

A centred panel shows a greeting, a pluralised item count, and the active language name in four locales (English, French, Japanese, Arabic). Click the locale buttons or press 1-4 to switch and the greeting, plural phrasing and script all change, including Japanese CJK glyphs and a right-aligned Arabic line. Adjust the item count to watch each locale’s plural category (English one/other, Arabic zero/one/two/few/many/other) select the correct phrase.

What it demonstrates

  • Loading translation tables and switching Translations.instance().locale live.

  • tr(key, **kwargs) lookup with {name} interpolation and locale fallback.

  • CLDR plural rules via translate_plural, incl. Arabic’s six plural categories.

  • Multi-script text (Latin, Japanese, Arabic) via the engine’s font fallback chain.

  • Anchored, resize-aware UI (centred Panel + VBox) with clickable locale buttons.

Controls: 1 / 2 / 3 / 4 - English / French / Japanese / Arabic (or click the buttons) Left / Right - Decrease / increase the item count (or the - / + buttons) ESC - Quit

Source

  1"""Internationalisation: switch UI language and watch plural rules update live.
  2
  3A centred panel shows a greeting, a pluralised item count, and the active
  4language name in four locales (English, French, Japanese, Arabic). Click the
  5locale buttons or press 1-4 to switch and the greeting, plural phrasing and
  6script all change, including Japanese CJK glyphs and a right-aligned Arabic
  7line. Adjust the item count to watch each locale's plural category (English
  8one/other, Arabic zero/one/two/few/many/other) select the correct phrase.
  9
 10# /// simvx
 11# tags = ["ui", "i18n", "localisation", "text", "plural"]
 12# web = { root = "I18nDemo", width = 720, height = 480, responsive = true }
 13# ///
 14
 15## What it demonstrates
 16- Loading translation tables and switching `Translations.instance().locale` live.
 17- `tr(key, **kwargs)` lookup with `{name}` interpolation and locale fallback.
 18- CLDR plural rules via `translate_plural`, incl. Arabic's six plural categories.
 19- Multi-script text (Latin, Japanese, Arabic) via the engine's font fallback chain.
 20- Anchored, resize-aware UI (centred Panel + VBox) with clickable locale buttons.
 21
 22Controls:
 23  1 / 2 / 3 / 4   - English / French / Japanese / Arabic (or click the buttons)
 24  Left / Right    - Decrease / increase the item count (or the - / + buttons)
 25  ESC             - Quit
 26"""
 27
 28from simvx.core import (
 29    AnchorPreset,
 30    Colour,
 31    Input,
 32    InputMap,
 33    Key,
 34    Label,
 35    Node,
 36    Panel,
 37    Vec2,
 38)
 39from simvx.core.i18n import Translations, tr
 40from simvx.core.ui import Button, HBoxContainer, VBoxContainer
 41from simvx.graphics import App
 42
 43# Translation data (inline: no external file needed)
 44TRANSLATIONS = {
 45    "en": {
 46        "greeting": "Hello, {name}!",
 47        "item_one": "You have {count} item.",
 48        "item_other": "You have {count} items.",
 49        "language": "Language: English",
 50        "instructions": "1=EN  2=FR  3=JA  4=AR   Left/Right = count   ESC = Quit",
 51    },
 52    "fr": {
 53        "greeting": "Bonjour, {name} !",
 54        "item_one": "Vous avez {count} objet.",
 55        "item_other": "Vous avez {count} objets.",
 56        "language": "Langue : Français",
 57        "instructions": "1=EN  2=FR  3=JA  4=AR   Gauche/Droite = nombre   ESC = Quitter",
 58    },
 59    "ja": {
 60        "greeting": "こんにちは、{name}さん!",
 61        "item_other": "アイテムが{count}個あります。",
 62        "language": "言語: 日本語",
 63        "instructions": "1=EN  2=FR  3=JA  4=AR   ←/→ = 数   ESC = 終了",
 64    },
 65    "ar": {
 66        "greeting": "مرحبا، {name}!",
 67        "item_zero": "ليس لديك عناصر.",
 68        "item_one": "لديك عنصر واحد.",
 69        "item_two": "لديك عنصران.",
 70        "item_few": "لديك {count} عناصر.",
 71        "item_many": "لديك {count} عنصرا.",
 72        "item_other": "لديك {count} عنصر.",
 73        "language": "اللغة: العربية",
 74        "instructions": "1=EN  2=FR  3=JA  4=AR   ←/→   ESC",
 75    },
 76}
 77
 78# Ordered locale list drives both the key bindings and the on-screen buttons.
 79LOCALES = [("en", "EN"), ("fr", "FR"), ("ja", "JA"), ("ar", "AR")]
 80_LOCALE_KEYS = {"en": Key.KEY_1, "fr": Key.KEY_2, "ja": Key.KEY_3, "ar": Key.KEY_4}
 81_RTL_LOCALES = {"ar"}
 82
 83PANEL_W, PANEL_H = 560.0, 340.0
 84
 85
 86class I18nDemo(Node):
 87    """Root scene: a centred panel that swaps its language and plural forms live."""
 88
 89    def on_ready(self):
 90        ts = Translations.instance()
 91        ts.load_dict(TRANSLATIONS)
 92        ts.locale = "en"
 93        self._item_count = 5
 94
 95        # Key bindings: 1-4 select a locale, arrows change the count, ESC quits.
 96        for code, key in _LOCALE_KEYS.items():
 97            InputMap.add_action(f"locale_{code}", [key])
 98        InputMap.add_action("count_down", [Key.LEFT])
 99        InputMap.add_action("count_up", [Key.RIGHT])
100        InputMap.add_action("quit", [Key.ESCAPE])
101
102        # Centred panel: symmetric margins around the screen centre keep it put
103        # at any window size, so the layout stays legible and resize-aware.
104        panel = Panel(name="Panel")
105        panel.set_anchor_preset(AnchorPreset.CENTER)
106        panel.margin_left = -PANEL_W / 2
107        panel.margin_right = PANEL_W / 2
108        panel.margin_top = -PANEL_H / 2
109        panel.margin_bottom = PANEL_H / 2
110        panel.bg_colour = Colour.hex("#1A1A2E")
111        self.add_child(panel)
112
113        # Vertical stack filling the panel with a small inset.
114        column = VBoxContainer(name="Column")
115        column.separation = 14.0
116        column.set_anchor_preset(AnchorPreset.FULL_RECT)
117        column.margin_left = 26
118        column.margin_right = -26
119        column.margin_top = 22
120        column.margin_bottom = -22
121        panel.add_child(column)
122
123        self._title = self._add_label(column, font_size=20.0, colour=Colour.WHITE, align="center")
124        self._greeting = self._add_label(column, font_size=18.0, colour=Colour.hex("#FFD166"))
125        self._plural = self._add_label(column, font_size=16.0, colour=Colour.WHITE)
126        self._language = self._add_label(column, font_size=14.0, colour=Colour.LIGHT_GRAY)
127
128        # Item-count row: minus / plus buttons flanking the live count.
129        count_row = HBoxContainer(name="CountRow")
130        count_row.separation = 10.0
131        minus = Button("-", on_press=lambda: self._change_count(-1))
132        minus.size = Vec2(40, 30)
133        count_row.add_child(minus)
134        self._count_label = Label("")
135        self._count_label.font_size = 14.0
136        self._count_label.size = Vec2(120, 30)
137        self._count_label.alignment = "center"
138        count_row.add_child(self._count_label)
139        plus = Button("+", on_press=lambda: self._change_count(1))
140        plus.size = Vec2(40, 30)
141        count_row.add_child(plus)
142        column.add_child(count_row)
143
144        # Clickable locale buttons mirror the 1-4 keys for mouse/touch play.
145        button_row = HBoxContainer(name="Locales")
146        button_row.separation = 8.0
147        self._locale_buttons: dict[str, Button] = {}
148        for code, short in LOCALES:
149            btn = Button(short, on_press=lambda c=code: self._set_locale(c))
150            btn.size = Vec2(108, 34)
151            self._locale_buttons[code] = btn
152            button_row.add_child(btn)
153        column.add_child(button_row)
154
155        self._help = self._add_label(column, font_size=12.0, colour=Colour.GRAY, align="center")
156
157        self._refresh()
158
159    @staticmethod
160    def _add_label(parent, *, font_size: float, colour, align: str = "left") -> Label:
161        label = Label("")
162        label.font_size = font_size
163        label.text_colour = colour
164        label.alignment = align
165        parent.add_child(label)
166        return label
167
168    def on_update(self, dt: float):
169        if Input.is_action_just_pressed("quit"):
170            self.app.quit()
171            return
172        for code, _short in LOCALES:
173            if Input.is_action_just_pressed(f"locale_{code}"):
174                self._set_locale(code)
175                break
176        if Input.is_action_just_pressed("count_up"):
177            self._change_count(1)
178        if Input.is_action_just_pressed("count_down"):
179            self._change_count(-1)
180
181    def _set_locale(self, code: str):
182        Translations.instance().locale = code
183        self._refresh()
184
185    def _change_count(self, delta: int):
186        self._item_count = max(0, self._item_count + delta)
187        self._refresh()
188
189    def _refresh(self):
190        ts = Translations.instance()
191        rtl = ts.locale in _RTL_LOCALES
192        # Right-align the translated body lines for right-to-left locales so the
193        # presentation reads in the correct direction.
194        body_align = "right" if rtl else "left"
195
196        self._title.text = f"SimVX i18n Demo [{ts.locale.upper()}]"
197        self._greeting.text = tr("greeting", name="Player")
198        self._plural.text = ts.translate_plural("item", self._item_count)
199        self._language.text = tr("language")
200        self._help.text = tr("instructions")
201        for label in (self._greeting, self._plural, self._language):
202            label.alignment = body_align
203
204        self._count_label.text = f"Items: {self._item_count}"
205
206        # Highlight the active locale button so the current language is obvious.
207        for code, btn in self._locale_buttons.items():
208            btn.set_visual_state_override("pressed" if code == ts.locale else None)
209
210
211if __name__ == "__main__":
212    App(title="SimVX i18n Demo", width=720, height=480).run(I18nDemo())