Coverage for app/backend/src/tests/test_i18next.py: 100%
142 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
1import babel
2import pytest
3from markupsafe import Markup
5from couchers.i18n.i18next import I18Next, LocalizationError, full_string_key
8def test_lookup():
9 i18next = I18Next()
10 i18next.add_translation("en").add_string("greeting", "hello")
11 assert i18next.localize("greeting", "en") == "hello"
14def test_substitution():
15 i18next = I18Next()
16 i18next.add_translation("en").add_string("greeting", "hello {{name}}!")
17 assert i18next.localize("greeting", "en", {"name": "world"}) == "hello world!"
20def test_placeholder_with_spacing():
21 i18next = I18Next()
22 i18next.add_translation("en").add_string("greeting", "hello {{ name }}!")
23 assert i18next.localize("greeting", "en", {"name": "world"}) == "hello world!"
26def test_localized():
27 i18next = I18Next()
28 en = i18next.add_translation("en")
29 en.add_string("greeting", "hello")
30 fr = i18next.add_translation("fr")
31 fr.add_string("greeting", "bonjour")
32 fr.fallbacks.append(en)
33 assert i18next.localize("greeting", "fr") == "bonjour"
36def test_fallback():
37 i18next = I18Next()
38 en = i18next.add_translation("en")
39 en.add_string("greeting", "hello")
40 fr = i18next.add_translation("fr")
41 fr.fallbacks.append(en)
42 assert i18next.localize("greeting", "fr") == "hello"
45def test_mutual_fallback():
46 i18next = I18Next()
47 pt_pt = i18next.add_translation("pt-PT")
48 pt_pt.add_string("greeting", "olá")
49 pt_br = i18next.add_translation("pt-BR")
50 pt_br.add_string("farewell", "tchau")
51 pt_pt.fallbacks.append(pt_br)
52 pt_br.fallbacks.append(pt_pt)
53 assert i18next.localize("greeting", "pt-BR") == "olá"
54 assert i18next.localize("farewell", "pt-PT") == "tchau"
57def test_plural_suffixes():
58 i18next = I18Next()
59 en = i18next.add_translation("en")
60 en.add_string("apples_one", "{{count}} apple")
61 en.add_string("apples_other", "{{count}} apples")
62 assert i18next.localize("apples", "en", {"count": 1}) == "1 apple"
63 assert i18next.localize("apples", "en", {"count": 2}) == "2 apples"
66def test_plural_suffix_fallback():
67 i18next = I18Next()
68 en = i18next.add_translation("en")
69 en.add_string("apples", "{{count}} apples")
70 en.add_string("apples_one", "{{count}} apple")
71 assert i18next.localize("apples", "en", {"count": 1}) == "1 apple"
72 assert i18next.localize("apples", "en", {"count": 2}) == "2 apples"
75def test_plural_no_count():
76 i18next = I18Next()
77 en = i18next.add_translation("en")
78 en.add_string("apples_one", "apple")
79 en.add_string("apples_other", "apples")
80 assert i18next.localize("apples", "en", {"count": 1}) == "apple"
81 assert i18next.localize("apples", "en", {"count": 2}) == "apples"
84def test_missing_babel_locale():
85 i18next = I18Next()
87 with pytest.raises(babel.UnknownLocaleError):
88 i18next.add_translation("piglatin")
91def test_load_simple_json():
92 i18next = I18Next()
93 en = i18next.add_translation("en")
94 en.load_json_dict({"greeting": "hello"})
95 assert i18next.localize("greeting", "en") == "hello"
98def test_load_nested_json():
99 i18next = I18Next()
100 en = i18next.add_translation("en")
101 en.load_json_dict({"greeting": {"short": "hi"}})
102 assert i18next.localize("greeting.short", "en") == "hi"
105def test_fallback_locale():
106 i18next = I18Next()
107 en = i18next.add_translation("en")
108 en.add_string("greeting", "hello")
109 i18next.default_translation = en
110 assert i18next.localize("greeting", "fr") == "hello"
113# An empty string in a translation should be considered as the lack of a string,
114# since this is how Weblate/i18next interpret it.
115def test_fallback_on_empty_string():
116 i18next = I18Next()
117 en = i18next.add_translation("en", json_dict={"greeting": "hello"})
118 fr = i18next.add_translation("fr", json_dict={"greeting": ""})
119 fr.fallbacks.append(en)
120 assert i18next.localize("greeting", "fr") == "hello"
123def test_missing_locale():
124 i18next = I18Next()
125 with pytest.raises(LocalizationError) as raised:
126 i18next.localize("greeting", "en")
127 assert raised.value.locale == "en"
128 assert raised.value.string_key == "greeting"
131def test_missing_string():
132 i18next = I18Next()
133 i18next.add_translation("en")
134 with pytest.raises(LocalizationError) as raised:
135 i18next.localize("greeting", "en")
136 assert raised.value.locale == "en"
137 assert raised.value.string_key == "greeting"
140def test_missing_plural_form():
141 i18next = I18Next()
142 en = i18next.add_translation("en")
143 en.add_string("apples_one", "{{count}} apple")
144 assert i18next.localize("apples", "en", {"count": 1}) == "1 apple"
145 with pytest.raises(LocalizationError) as raised:
146 i18next.localize("apples", "en", {"count": 2})
147 assert raised.value.locale == "en"
148 assert raised.value.string_key == "apples"
151def test_extra_substitution():
152 i18next = I18Next()
153 i18next.add_translation("en").add_string("greeting", "hello")
154 assert i18next.localize("greeting", "en", substitutions={"e": "mc2"})
157def test_missing_substitution():
158 i18next = I18Next()
159 i18next.add_translation("en").add_string("greeting", "hello {{name}}")
160 with pytest.raises(LocalizationError) as raised:
161 i18next.localize("greeting", "en")
162 assert raised.value.locale == "en"
163 assert raised.value.string_key == "greeting"
166def test_missing_substitution_fallback():
167 i18next = I18Next()
168 en = i18next.add_translation("en")
169 en.add_string("greeting", "hello {{name}}")
170 fr = i18next.add_translation("fr")
171 fr.add_string("greeting", "bonjour {{nom}}")
172 fr.fallbacks.append(en)
173 assert i18next.localize("greeting", "fr", substitutions={"name": "world"}) == "hello world"
176def test_escaping():
177 i18next = I18Next()
178 i18next.add_translation("en", json_dict={"greeting": "hello {{name}}"})
180 # localize returns an str, which is considered untrusted for markup,
181 # so it can contain tags because the renderer is resposible for escaping them.
182 # Markup in this context is unescaped back into plaintext to avoid double-escaping.
183 assert i18next.localize("greeting", "en", substitutions={"name": "<script/>"}) == "hello <script/>"
184 assert i18next.localize("greeting", "en", substitutions={"name": Markup("<script/>")}) == "hello <script/>"
186 # localize_with_markup returns a Markup object, which is considered trusted for markup,
187 # so it can only interpolate tags if they are also trusted, and otherwise will escape them.
188 assert (
189 i18next.localize_with_markup("greeting", "en", substitutions={"name": "<script/>"}) == "hello <script/>"
190 )
191 assert (
192 i18next.localize_with_markup("greeting", "en", substitutions={"name": Markup("<script/>")}) == "hello <script/>"
193 )
196def test_full_string_key():
197 assert full_string_key("key", relative_base=None) == "key"
198 assert full_string_key("key", relative_base="base") == "key"
199 assert full_string_key(".key", relative_base="base") == "base.key"
200 with pytest.raises(ValueError):
201 assert full_string_key(".key", relative_base=None)