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

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 assert i18next.localize("greeting", ["fr", "en"]) == "bonjour" 

33 

34 

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" 

41 

42 

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" 

51 

52 

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" 

60 

61 

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" 

69 

70 

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" 

78 

79 

80def test_missing_babel_locale(): 

81 i18next = I18Next() 

82 

83 with pytest.raises(babel.UnknownLocaleError): 

84 i18next.add_translation("piglatin") 

85 

86 

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" 

92 

93 

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" 

99 

100 

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" 

108 

109 

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" 

116 

117 

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" 

125 

126 

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" 

136 

137 

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

142 

143 

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" 

151 

152 

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" 

160 

161 

162def test_escaping(): 

163 i18next = I18Next() 

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

165 

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

171 

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

176 ) 

177 assert ( 

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

179 == "hello <script/>" 

180 ) 

181 

182 

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)