Coverage for app / backend / src / tests / test_i18next.py: 100%
137 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
1import pytest
2from markupsafe import Markup
4from couchers.i18n.i18next import I18Next, LocalizationError
7def test_lookup():
8 i18next = I18Next()
9 i18next.add_translation("en").add_string("greeting", "hello")
10 assert i18next.localize("greeting", "en") == "hello"
13def test_substitution():
14 i18next = I18Next()
15 i18next.add_translation("en").add_string("greeting", "hello {{name}}!")
16 assert i18next.localize("greeting", "en", {"name": "world"}) == "hello world!"
19def test_placeholder_with_spacing():
20 i18next = I18Next()
21 i18next.add_translation("en").add_string("greeting", "hello {{ name }}!")
22 assert i18next.localize("greeting", "en", {"name": "world"}) == "hello world!"
25def test_localized():
26 i18next = I18Next()
27 en = i18next.add_translation("en")
28 en.add_string("greeting", "hello")
29 fr = i18next.add_translation("fr")
30 fr.add_string("greeting", "bonjour")
31 fr.fallbacks.append(en)
32 assert i18next.localize("greeting", "fr") == "bonjour"
35def test_fallback():
36 i18next = I18Next()
37 en = i18next.add_translation("en")
38 en.add_string("greeting", "hello")
39 fr = i18next.add_translation("fr")
40 fr.fallbacks.append(en)
41 assert i18next.localize("greeting", "fr") == "hello"
44def test_mutual_fallback():
45 i18next = I18Next()
46 pt_pt = i18next.add_translation("pt-PT")
47 pt_pt.add_string("greeting", "olá")
48 pt_br = i18next.add_translation("pt-BR")
49 pt_br.add_string("farewell", "tchau")
50 pt_pt.fallbacks.append(pt_br)
51 pt_br.fallbacks.append(pt_pt)
52 assert i18next.localize("greeting", "pt-BR") == "olá"
53 assert i18next.localize("farewell", "pt-PT") == "tchau"
56def test_plural_suffixes():
57 i18next = I18Next()
58 en = i18next.add_translation("en")
59 en.add_string("apples_one", "{{count}} apple")
60 en.add_string("apples_other", "{{count}} apples")
61 assert i18next.localize("apples", "en", {"count": 1}) == "1 apple"
62 assert i18next.localize("apples", "en", {"count": 2}) == "2 apples"
65def test_plural_suffix_fallback():
66 i18next = I18Next()
67 en = i18next.add_translation("en")
68 en.add_string("apples", "{{count}} apples")
69 en.add_string("apples_one", "{{count}} apple")
70 assert i18next.localize("apples", "en", {"count": 1}) == "1 apple"
71 assert i18next.localize("apples", "en", {"count": 2}) == "2 apples"
74def test_plural_no_count():
75 i18next = I18Next()
76 en = i18next.add_translation("en")
77 en.add_string("apples_one", "apple")
78 en.add_string("apples_other", "apples")
79 assert i18next.localize("apples", "en", {"count": 1}) == "apple"
80 assert i18next.localize("apples", "en", {"count": 2}) == "apples"
83def test_missing_plural_rules():
84 i18next = I18Next()
85 piglatin = i18next.add_translation("piglatin", json_dict={"pigs": "igpays", "pigs_one": "igpay"})
86 en = i18next.add_translation("en", json_dict={"pigs": "pigs", "pigs_one": "pig"})
87 piglatin.fallbacks.append(en)
88 # Should resolve using the english plural rules since "piglatin" doesn't have its own.
89 assert i18next.localize("pigs", "piglatin", {"count": 1}) == "igpay"
92def test_load_simple_json():
93 i18next = I18Next()
94 en = i18next.add_translation("en")
95 en.load_json_dict({"greeting": "hello"})
96 assert i18next.localize("greeting", "en") == "hello"
99def test_load_nested_json():
100 i18next = I18Next()
101 en = i18next.add_translation("en")
102 en.load_json_dict({"greeting": {"short": "hi"}})
103 assert i18next.localize("greeting.short", "en") == "hi"
106def test_fallback_locale():
107 i18next = I18Next()
108 en = i18next.add_translation("en")
109 en.add_string("greeting", "hello")
110 i18next.default_translation = en
111 assert i18next.localize("greeting", "fr") == "hello"
114# An empty string in a translation should be considered as the lack of a string,
115# since this is how Weblate/i18next interpret it.
116def test_fallback_on_empty_string():
117 i18next = I18Next()
118 en = i18next.add_translation("en", json_dict={"greeting": "hello"})
119 fr = i18next.add_translation("fr", json_dict={"greeting": ""})
120 fr.fallbacks.append(en)
121 assert i18next.localize("greeting", "fr") == "hello"
124def test_missing_locale():
125 i18next = I18Next()
126 with pytest.raises(LocalizationError) as raised:
127 i18next.localize("greeting", "en")
128 assert raised.value.locale == "en"
129 assert raised.value.string_key == "greeting"
132def test_missing_string():
133 i18next = I18Next()
134 i18next.add_translation("en")
135 with pytest.raises(LocalizationError) as raised:
136 i18next.localize("greeting", "en")
137 assert raised.value.locale == "en"
138 assert raised.value.string_key == "greeting"
141def test_missing_plural_form():
142 i18next = I18Next()
143 en = i18next.add_translation("en")
144 en.add_string("apples_one", "{{count}} apple")
145 assert i18next.localize("apples", "en", {"count": 1}) == "1 apple"
146 with pytest.raises(LocalizationError) as raised:
147 i18next.localize("apples", "en", {"count": 2})
148 assert raised.value.locale == "en"
149 assert raised.value.string_key == "apples"
152def test_extra_substitution():
153 i18next = I18Next()
154 i18next.add_translation("en").add_string("greeting", "hello")
155 assert i18next.localize("greeting", "en", substitutions={"e": "mc2"})
158def test_missing_substitution():
159 i18next = I18Next()
160 i18next.add_translation("en").add_string("greeting", "hello {{name}}")
161 with pytest.raises(LocalizationError) as raised:
162 i18next.localize("greeting", "en")
163 assert raised.value.locale == "en"
164 assert raised.value.string_key == "greeting"
167def test_missing_substitution_fallback():
168 i18next = I18Next()
169 en = i18next.add_translation("en")
170 en.add_string("greeting", "hello {{name}}")
171 fr = i18next.add_translation("fr")
172 fr.add_string("greeting", "bonjour {{nom}}")
173 fr.fallbacks.append(en)
174 assert i18next.localize("greeting", "fr", substitutions={"name": "world"}) == "hello world"
177def test_escaping():
178 i18next = I18Next()
179 i18next.add_translation("en", json_dict={"greeting": "hello {{name}}"})
181 # localize returns an str, which is considered untrusted for markup,
182 # so it can contain tags because the renderer is resposible for escaping them.
183 # Markup in this context is unescaped back into plaintext to avoid double-escaping.
184 assert i18next.localize("greeting", "en", substitutions={"name": "<script/>"}) == "hello <script/>"
185 assert i18next.localize("greeting", "en", substitutions={"name": Markup("<script/>")}) == "hello <script/>"
187 # localize_with_markup returns a Markup object, which is considered trusted for markup,
188 # so it can only interpolate tags if they are also trusted, and otherwise will escape them.
189 assert (
190 i18next.localize_with_markup("greeting", "en", substitutions={"name": "<script/>"}) == "hello <script/>"
191 )
192 assert (
193 i18next.localize_with_markup("greeting", "en", substitutions={"name": Markup("<script/>")}) == "hello <script/>"
194 )