Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/modules/translation.py: 0%

72 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-27 07:59 +0000

1import enum 

2import fnmatch 

3import json 

4import logging 

5import os 

6from datetime import timedelta as td 

7from viur.core import conf, db, utils, current, errors 

8from viur.core.decorators import exposed 

9from viur.core.bones import * 

10from viur.core.i18n import KINDNAME, initializeTranslations, systemTranslations, translate 

11from viur.core.prototypes.list import List 

12from viur.core.skeleton import Skeleton, SkeletonInstance, ViurTagsSearchAdapter 

13 

14 

15class Creator(enum.Enum): 

16 VIUR = "viur" 

17 USER = "user" 

18 

19 

20class TranslationSkel(Skeleton): 

21 kindName = KINDNAME 

22 

23 database_adapters = [ 

24 ViurTagsSearchAdapter(max_length=256), 

25 ] 

26 

27 name = StringBone( 

28 descr="Name", 

29 visible=False, 

30 compute=Compute( 

31 fn=lambda skel: str(skel["tr_key"]), 

32 interval=ComputeInterval(ComputeMethod.OnWrite), 

33 ), 

34 ) 

35 

36 tr_key = StringBone( 

37 descr=translate( 

38 "core.translationskel.tr_key.descr", 

39 "Translation key", 

40 ), 

41 searchable=True, 

42 escape_html=False, 

43 required=True, 

44 min_length=1, 

45 unique=UniqueValue(UniqueLockMethod.SameValue, False, 

46 "This translation key exist already"), 

47 ) 

48 

49 translations = StringBone( 

50 descr=translate( 

51 "core.translationskel.translations.descr", 

52 "Translations", 

53 ), 

54 searchable=True, 

55 languages=conf.i18n.available_dialects, 

56 escape_html=False, 

57 max_length=1024, 

58 params={ 

59 "tooltip": translate( 

60 "core.translationskel.translations.tooltip", 

61 "The languages {{main}} are required,\n {{accent}} can be filled out" 

62 )(main=", ".join(conf.i18n.available_languages), 

63 accent=", ".join(conf.i18n.language_alias_map.keys())), 

64 } 

65 ) 

66 

67 translations_missing = SelectBone( 

68 descr=translate( 

69 "core.translationskel.translations_missing.descr", 

70 "Translation missing for language", 

71 ), 

72 multiple=True, 

73 readOnly=True, 

74 values=conf.i18n.available_dialects, 

75 compute=Compute( 

76 fn=lambda skel: [lang 

77 for lang in conf.i18n.available_dialects 

78 if not skel["translations"].get(lang)], 

79 interval=ComputeInterval(ComputeMethod.OnWrite), 

80 ), 

81 ) 

82 

83 default_text = StringBone( 

84 escape_html=False, 

85 descr=translate( 

86 "core.translationskel.default_text.descr", 

87 "Fallback value", 

88 ), 

89 ) 

90 

91 hint = StringBone( 

92 descr=translate( 

93 "core.translationskel.hint.descr", 

94 "Hint / Context (internal only)", 

95 ), 

96 ) 

97 

98 usage_filename = StringBone( 

99 descr=translate( 

100 "core.translationskel.usage_filename.descr", 

101 "Used and added from this file", 

102 ), 

103 readOnly=True, 

104 ) 

105 

106 usage_lineno = NumericBone( 

107 descr=translate( 

108 "core.translationskel.usage_lineno.descr", 

109 "Used and added from this lineno", 

110 ), 

111 readOnly=True, 

112 ) 

113 

114 usage_variables = StringBone( 

115 descr=translate( 

116 "core.translationskel.usage_variables.descr", 

117 "Receives these substitution variables", 

118 ), 

119 readOnly=True, 

120 multiple=True, 

121 ) 

122 

123 creator = SelectBone( 

124 descr=translate( 

125 "core.translationskel.creator.descr", 

126 "Creator", 

127 ), 

128 readOnly=True, 

129 values=Creator, 

130 defaultValue=Creator.USER, 

131 ) 

132 

133 public = BooleanBone( 

134 descr=translate( 

135 "core.translationskel.public.descr", 

136 "Is this translation public?", 

137 ), 

138 defaultValue=False, 

139 ) 

140 

141 @classmethod 

142 def write(cls, skelValues: SkeletonInstance, **kwargs) -> db.Key: 

143 # Ensure we have only lowercase keys 

144 skelValues["tr_key"] = skelValues["tr_key"].lower() 

145 return super().write(skelValues, **kwargs) 

146 

147 @classmethod 

148 def preProcessSerializedData(cls, skelValues: SkeletonInstance, entity: db.Entity) -> db.Entity: 

149 # Backward-compatibility: re-add the key for viur-core < v3.6 

150 # TODO: Remove in ViUR4 

151 entity["key"] = skelValues["tr_key"] 

152 return super().preProcessSerializedData(skelValues, entity) 

153 

154 

155class Translation(List): 

156 """ 

157 The Translation module is a system module used by the ViUR framework for its internationalization capabilities. 

158 """ 

159 

160 kindName = KINDNAME 

161 

162 def adminInfo(self): 

163 return { 

164 "name": translate("translations"), 

165 "icon": "translate", 

166 "display": "hidden" if len(conf.i18n.available_dialects) <= 1 else "default", 

167 "views": [ 

168 { 

169 "name": translate( 

170 "core.translations.view.missing", 

171 "Missing translations for {{lang}}", 

172 )(lang=lang), 

173 "filter": { 

174 "translations_missing": lang, 

175 }, 

176 } 

177 for lang in conf.i18n.available_dialects 

178 ], 

179 } 

180 

181 roles = { 

182 "admin": "*", 

183 } 

184 

185 def onAdded(self, *args, **kwargs): 

186 super().onAdded(*args, **kwargs) 

187 self._reload_translations() 

188 

189 def onEdited(self, *args, **kwargs): 

190 super().onEdited(*args, **kwargs) 

191 self._reload_translations() 

192 

193 def onDeleted(self, *args, **kwargs): 

194 super().onDeleted(*args, **kwargs) 

195 self._reload_translations() 

196 

197 def _reload_translations(self): 

198 if ( 

199 self._last_reload is not None 

200 and self._last_reload - utils.utcNow() < td(minutes=10) 

201 ): 

202 # debounce: translations has been reload recently, skip this 

203 return None 

204 logging.info("Reload translations") 

205 # TODO: this affects only the current instance 

206 self._last_reload = utils.utcNow() 

207 systemTranslations.clear() 

208 initializeTranslations() 

209 

210 _last_reload = None # Cut my strings into pieces, this is my last reload... 

211 

212 @exposed 

213 def get_public( 

214 self, 

215 *, 

216 languages: list[str] = [], 

217 pattern: str = "*", 

218 ) -> dict[str, str] | dict[str, dict[str, str]]: 

219 """ 

220 Dumps public translations as JSON. 

221 

222 :param languages: Allows to request a specific language. 

223 :param pattern: Provide an fnmatch-style key filter pattern 

224 

225 Example calls: 

226 

227 - `/json/_translation/get_public` get public translations for current language 

228 - `/json/_translation/get_public?languages=en` for english translations 

229 - `/json/_translation/get_public?languages=en&pattern=bool.*` for english translations, 

230 but only keys starting with "bool." 

231 - `/json/_translation/get_public?languages=en&languages=de` for english and german translations 

232 - `/json/_translation/get_public?languages=*` for all available languages 

233 """ 

234 if not utils.string.is_prefix(self.render.kind, "json"): 

235 raise errors.BadRequest("Can only use this function on JSON-based renders") 

236 

237 current.request.get().response.headers["Content-Type"] = "application/json" 

238 

239 if ( 

240 not (conf.debug.disable_cache and current.request.get().disableCache) 

241 and any(os.getenv("HTTP_HOST", "") in x for x in conf.i18n.domain_language_mapping) 

242 ): 

243 # cache it 7 days 

244 current.request.get().response.headers["Cache-Control"] = f"public, max-age={7 * 24 * 60 * 60}" 

245 

246 if languages: 

247 if len(languages) == 1 and languages[0] == "*": 

248 languages = conf.i18n.available_dialects 

249 

250 return json.dumps({ 

251 lang: { 

252 tr_key: str(translate(tr_key, force_lang=lang)) 

253 for tr_key, values in systemTranslations.items() 

254 if values.get("_public_") and fnmatch.fnmatch(tr_key, pattern) 

255 } 

256 for lang in languages 

257 }) 

258 

259 return json.dumps({ 

260 tr_key: str(translate(tr_key)) 

261 for tr_key, values in systemTranslations.items() 

262 if values.get("_public_") and fnmatch.fnmatch(tr_key, pattern) 

263 }) 

264 

265 

266Translation.json = True