Coverage for app/backend/src/tests/test_i18next.py: 100%
130 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-21 21:52 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-21 21:52 +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 assert i18next.localize("greeting", ["fr", "en"]) == "bonjour"
35def test_fallback():
36 i18next = I18Next()
37 en = i18next.add_translation("en")
38 en.add_string("greeting", "hello")
39 i18next.add_translation("fr")
40 assert i18next.localize("greeting", ["fr", "en"]) == "hello"
43def test_mutual_fallback():
44 i18next = I18Next()
45 pt_pt = i18next.add_translation("pt-PT")
46 pt_pt.add_string("greeting", "olá")
47 pt_br = i18next.add_translation("pt-BR")
48 pt_br.add_string("farewell", "tchau")
49 assert i18next.localize("greeting", ["pt-BR", "pt-PT"]) == "olá"
50 assert i18next.localize("farewell", ["pt-PT", "pt-BR"]) == "tchau"
53def test_plural_suffixes():
54 i18next = I18Next()
55 en = i18next.add_translation("en")
56 en.add_string("apples_one", "{{count}} apple")
57 en.add_string("apples_other", "{{count}} apples")
58 assert i18next.localize("apples", ["en"], {"count": 1}) == "1 apple"
59 assert i18next.localize("apples", ["en"], {"count": 2}) == "2 apples"
62def test_plural_suffix_fallback():
63 i18next = I18Next()
64 en = i18next.add_translation("en")
65 en.add_string("apples", "{{count}} apples")
66 en.add_string("apples_one", "{{count}} apple")
67 assert i18next.localize("apples", ["en"], {"count": 1}) == "1 apple"
68 assert i18next.localize("apples", ["en"], {"count": 2}) == "2 apples"
71def test_plural_no_count():
72 i18next = I18Next()
73 en = i18next.add_translation("en")
74 en.add_string("apples_one", "apple")
75 en.add_string("apples_other", "apples")
76 assert i18next.localize("apples", ["en"], {"count": 1}) == "apple"
77 assert i18next.localize("apples", ["en"], {"count": 2}) == "apples"
80def test_missing_babel_locale():
81 i18next = I18Next()
83 with pytest.raises(babel.UnknownLocaleError):
84 i18next.add_translation("piglatin")
87def test_load_simple_json():
88 i18next = I18Next()
89 en = i18next.add_translation("en")
90 en.load_json_dict({"greeting": "hello"})
91 assert i18next.localize("greeting", ["en"]) == "hello"
94def test_load_nested_json():
95 i18next = I18Next()
96 en = i18next.add_translation("en")
97 en.load_json_dict({"greeting": {"short": "hi"}})
98 assert i18next.localize("greeting.short", ["en"]) == "hi"
101# An empty string in a translation should be considered as the lack of a string,
102# since this is how Weblate/i18next interpret it.
103def test_fallback_on_empty_string():
104 i18next = I18Next()
105 en = i18next.add_translation("en", json_dict={"greeting": "hello"})
106 i18next.add_translation("fr", json_dict={"greeting": ""})
107 assert i18next.localize("greeting", ["fr", "en"]) == "hello"
110def test_missing_locale():
111 i18next = I18Next()
112 with pytest.raises(LocalizationError) as raised:
113 i18next.localize("greeting", ["en"])
114 assert raised.value.locales == ["en"]
115 assert raised.value.string_key == "greeting"
118def test_missing_string():
119 i18next = I18Next()
120 i18next.add_translation("en")
121 with pytest.raises(LocalizationError) as raised:
122 i18next.localize("greeting", ["en"])
123 assert raised.value.locales == ["en"]
124 assert raised.value.string_key == "greeting"
127def test_missing_plural_form():
128 i18next = I18Next()
129 en = i18next.add_translation("en")
130 en.add_string("apples_one", "{{count}} apple")
131 assert i18next.localize("apples", ["en"], {"count": 1}) == "1 apple"
132 with pytest.raises(LocalizationError) as raised:
133 i18next.localize("apples", ["en"], {"count": 2})
134 assert raised.value.locales == ["en"]
135 assert raised.value.string_key == "apples"
138def test_extra_substitution():
139 i18next = I18Next()
140 i18next.add_translation("en").add_string("greeting", "hello")
141 assert i18next.localize("greeting", ["en"], substitutions={"e": "mc2"})
144def test_missing_substitution():
145 i18next = I18Next()
146 i18next.add_translation("en").add_string("greeting", "hello {{name}}")
147 with pytest.raises(LocalizationError) as raised:
148 i18next.localize("greeting", ["en"])
149 assert raised.value.locales == ["en"]
150 assert raised.value.string_key == "greeting"
153def test_missing_substitution_fallback():
154 i18next = I18Next()
155 en = i18next.add_translation("en")
156 en.add_string("greeting", "hello {{name}}")
157 fr = i18next.add_translation("fr")
158 fr.add_string("greeting", "bonjour {{nom}}")
159 assert i18next.localize("greeting", ["fr", "en"], substitutions={"name": "world"}) == "hello world"
162def test_escaping():
163 i18next = I18Next()
164 i18next.add_translation("en", json_dict={"greeting": "hello {{name}}"})
166 # localize returns an str, which is considered untrusted for markup,
167 # so it can contain tags because the renderer is resposible for escaping them.
168 # Markup in this context is unescaped back into plaintext to avoid double-escaping.
169 assert i18next.localize("greeting", ["en"], substitutions={"name": "<script/>"}) == "hello <script/>"
170 assert i18next.localize("greeting", ["en"], substitutions={"name": Markup("<script/>")}) == "hello <script/>"
172 # localize_with_markup returns a Markup object, which is considered trusted for markup,
173 # so it can only interpolate tags if they are also trusted, and otherwise will escape them.
174 assert (
175 i18next.localize_with_markup("greeting", ["en"], substitutions={"name": "<script/>"}) == "hello <script/>"
176 )
177 assert (
178 i18next.localize_with_markup("greeting", ["en"], substitutions={"name": Markup("<script/>")})
179 == "hello <script/>"
180 )
183def test_full_string_key():
184 assert full_string_key("key", relative_base=None) == "key"
185 assert full_string_key("key", relative_base="base") == "key"
186 assert full_string_key(".key", relative_base="base") == "base.key"
187 with pytest.raises(ValueError):
188 assert full_string_key(".key", relative_base=None)