Internationalisation¶
switch UI language and watch plural rules update live.
▶ Run in browserTags: 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().localelive.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())