Internationalisation

switch UI language and watch plural rules update live.

▶ Run in browser

Tags: ui i18n localisation text plural

Translations is the engine’s translation table: load_dict({locale: {key: text}}) (or load_json / load_csv) fills it, and setting Translations.instance().locale switches the active language. tr(key, **kwargs) looks a key up in the active locale, falls back to the default locale when it is missing, and fills {name} placeholders from the keyword arguments. translate_plural(key, count) picks key_<category> by the locale’s CLDR plural rule: English has one and other, Japanese only other, and Arabic six categories, zero, one, two, few, many and other, each with its own phrase here.

The panel shows a greeting, a pluralised item count and the language name in English, French, Japanese and Arabic. Press 1-4 or click the EN / FR / JA / AR buttons to switch; press Left / Right or click - / + to change the count and watch the plural phrase change with it. Japanese and Arabic glyphs come from the engine’s font fallback chain, so no font is loaded here.

A label shows whatever string it was given, so refresh re-runs every lookup after a change. The engine has no text-direction data either, so the example keeps its own set of right-to-left locales and right-aligns the Arabic lines. The locale buttons share a ButtonGroup, which keeps the active one checked.

Source

  1# /// simvx
  2# tags = ["ui", "i18n", "localisation", "text", "plural"]
  3# web = { width = 720, height = 480, responsive = true }
  4# ///
  5"""Internationalisation: switch UI language and watch plural rules update live.
  6
  7`Translations` is the engine's translation table: `load_dict({locale: {key: text}})`
  8(or `load_json` / `load_csv`) fills it, and setting `Translations.instance().locale`
  9switches the active language. `tr(key, **kwargs)` looks a key up in the active
 10locale, falls back to the default locale when it is missing, and fills `{name}`
 11placeholders from the keyword arguments. `translate_plural(key, count)` picks
 12`key_<category>` by the locale's CLDR plural rule: English has `one` and `other`,
 13Japanese only `other`, and Arabic six categories, `zero`, `one`, `two`, `few`,
 14`many` and `other`, each with its own phrase here.
 15
 16The panel shows a greeting, a pluralised item count and the language name in
 17English, French, Japanese and Arabic. Press 1-4 or click the EN / FR / JA / AR
 18buttons to switch; press Left / Right or click - / + to change the count and watch
 19the plural phrase change with it. Japanese and Arabic glyphs come from the
 20engine's font fallback chain, so no font is loaded here.
 21
 22A label shows whatever string it was given, so `refresh` re-runs every lookup
 23after a change. The engine has no text-direction data either, so the example
 24keeps its own set of right-to-left locales and right-aligns the Arabic lines.
 25The locale buttons share a `ButtonGroup`, which keeps the active one checked.
 26"""
 27
 28from simvx.core import Anchor, Colour, HAlign, Input, Key, Label, Node, Panel
 29from simvx.core.i18n import Translations, tr
 30from simvx.core.ui import Button, ButtonGroup, HBoxContainer, VBoxContainer
 31from simvx.graphics import App
 32
 33TRANSLATIONS = {
 34    "en": {
 35        "greeting": "Hello, {name}!",
 36        "item_one": "You have {count} item.",
 37        "item_other": "You have {count} items.",
 38        "language": "Language: English",
 39        "count_label": "Items: {count}",
 40        "instructions": "1=EN  2=FR  3=JA  4=AR   Left/Right = count",
 41    },
 42    "fr": {
 43        "greeting": "Bonjour, {name} !",
 44        "item_one": "Vous avez {count} objet.",
 45        "item_other": "Vous avez {count} objets.",
 46        "language": "Langue : Français",
 47        "count_label": "Objets : {count}",
 48        "instructions": "1=EN  2=FR  3=JA  4=AR   Gauche/Droite = nombre",
 49    },
 50    "ja": {
 51        "greeting": "こんにちは、{name}さん!",
 52        "item_other": "アイテムが{count}個あります。",
 53        "language": "言語: 日本語",
 54        "count_label": "個数: {count}",
 55        "instructions": "1=EN  2=FR  3=JA  4=AR   ←/→ = 数",
 56    },
 57    "ar": {
 58        "greeting": "مرحبا، {name}!",
 59        "item_zero": "ليس لديك عناصر.",
 60        "item_one": "لديك عنصر واحد.",
 61        "item_two": "لديك عنصران.",
 62        "item_few": "لديك {count} عناصر.",
 63        "item_many": "لديك {count} عنصرا.",
 64        "item_other": "لديك {count} عنصر.",
 65        "language": "اللغة: العربية",
 66        "count_label": "العدد: {count}",
 67        "instructions": "1=EN  2=FR  3=JA  4=AR   ←/→",
 68    },
 69}
 70LOCALE_KEYS = {"en": Key.KEY_1, "fr": Key.KEY_2, "ja": Key.KEY_3, "ar": Key.KEY_4}
 71RIGHT_TO_LEFT = {"ar"}
 72
 73
 74class I18nDemo(Node):
 75    input_context = {f"locale_{code}": [key] for code, key in LOCALE_KEYS.items()} | {
 76        "fewer": [Key.LEFT],
 77        "more": [Key.RIGHT],
 78    }
 79
 80    def on_ready(self):
 81        Translations.instance().load_dict(TRANSLATIONS)
 82        self.count = 5
 83
 84        panel = self.add_child(Panel(anchor=Anchor.CENTER, size=(560, 340)))
 85        panel.bg_colour = Colour.hex("#1A1A2E")
 86        column = panel.add_child(VBoxContainer(anchor=Anchor.FULL_RECT, inset=(26, 22), separation=14))
 87
 88        self.title = column.add_child(Label(font_size=20, alignment=HAlign.CENTRE))
 89        self.greeting = column.add_child(Label(name="Greeting", font_size=18))
 90        self.greeting.text_colour = Colour.hex("#FFD166")
 91        self.plural = column.add_child(Label(name="Plural", font_size=16))
 92        self.language = column.add_child(Label(name="Language"))
 93        self.language.text_colour = Colour.LIGHT_GRAY
 94
 95        count_row = column.add_child(HBoxContainer(separation=10))
 96        count_row.add_child(Button("-", name="Fewer", size=(40, 30), on_press=lambda: self.change_count(-1)))
 97        self.count_label = count_row.add_child(Label(name="Count", size=(120, 30), alignment=HAlign.CENTRE))
 98        count_row.add_child(Button("+", name="More", size=(40, 30), on_press=lambda: self.change_count(1)))
 99
100        locale_row = column.add_child(HBoxContainer(separation=8))
101        group = ButtonGroup()
102        self.locale_buttons = {
103            code: locale_row.add_child(
104                Button(code.upper(), name=code, size=(108, 34), group=group, on_press=lambda c=code: self.set_locale(c))
105            )
106            for code in LOCALE_KEYS
107        }
108
109        self.help = column.add_child(Label(font_size=12, alignment=HAlign.CENTRE))
110        self.help.text_colour = Colour.GRAY
111        self.set_locale("en")
112
113    def on_update(self, dt):
114        for code in LOCALE_KEYS:
115            if Input.just_pressed(f"locale_{code}"):
116                self.set_locale(code)
117        if Input.just_pressed("fewer"):
118            self.change_count(-1)
119        if Input.just_pressed("more"):
120            self.change_count(1)
121
122    def set_locale(self, code: str):
123        Translations.instance().locale = code
124        self.refresh()
125
126    def change_count(self, delta: int):
127        self.count = max(0, self.count + delta)
128        self.refresh()
129
130    def refresh(self):
131        ts = Translations.instance()
132        self.title.text = f"SimVX i18n Demo [{ts.locale.upper()}]"
133        self.greeting.text = tr("greeting", name="Player")
134        self.plural.text = ts.translate_plural("item", self.count)
135        self.language.text = tr("language")
136        self.count_label.text = tr("count_label", count=self.count)
137        self.help.text = tr("instructions")
138        align = HAlign.RIGHT if ts.locale in RIGHT_TO_LEFT else HAlign.LEFT
139        for label in (self.greeting, self.plural, self.language):
140            label.alignment = align
141        self.locale_buttons[ts.locale].checked = True
142
143
144if __name__ == "__main__":
145    App(title="SimVX i18n Demo", width=720, height=480).run(I18nDemo())