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

1import babel 

2import pytest 

3from markupsafe import Markup 

4 

5from couchers.i18n.i18next import I18Next, LocalizationError, full_string_key 

6 

7 

8def test_lookup(): 

9 i18next = I18Next() 

10 i18next.add_translation("en").add_string("greeting", "hello") 

11 assert i18next.localize("greeting", "en") == "hello" 

12 

13 

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!" 

18 

19 

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!" 

24 

25 

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" 

34 

35 

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" 

43 

44 

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" 

55 

56 

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" 

64 

65 

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" 

73 

74 

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" 

82 

83 

84def test_missing_babel_locale(): 

85 i18next = I18Next() 

86 

87 with pytest.raises(babel.UnknownLocaleError): 

88 i18next.add_translation("piglatin") 

89 

90 

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" 

96 

97 

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" 

103 

104 

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" 

111 

112 

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" 

121 

122 

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" 

129 

130 

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" 

138 

139 

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" 

149 

150 

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"}) 

155 

156 

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" 

164 

165 

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" 

174 

175 

176def test_escaping(): 

177 i18next = I18Next() 

178 i18next.add_translation("en", json_dict={"greeting": "hello {{name}}"}) 

179 

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("&lt;script/&gt;")}) == "hello <script/>" 

185 

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 &lt;script/&gt;" 

190 ) 

191 assert ( 

192 i18next.localize_with_markup("greeting", "en", substitutions={"name": Markup("<script/>")}) == "hello <script/>" 

193 ) 

194 

195 

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)