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

1import pytest 

2from markupsafe import Markup 

3 

4from couchers.i18n.i18next import I18Next, LocalizationError 

5 

6 

7def test_lookup(): 

8 i18next = I18Next() 

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

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

11 

12 

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

17 

18 

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

23 

24 

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" 

33 

34 

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" 

42 

43 

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" 

54 

55 

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" 

63 

64 

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" 

72 

73 

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" 

81 

82 

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" 

90 

91 

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" 

97 

98 

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" 

104 

105 

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" 

112 

113 

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" 

122 

123 

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" 

130 

131 

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" 

139 

140 

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" 

150 

151 

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

156 

157 

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" 

165 

166 

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" 

175 

176 

177def test_escaping(): 

178 i18next = I18Next() 

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

180 

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

186 

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

191 ) 

192 assert ( 

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

194 )