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

87 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-03 12:27 +0000

1import enum 

2import fnmatch 

3import json 

4import logging 

5import os 

6import datetime 

7from deprecated.sphinx import deprecated 

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

9from viur.core.decorators import exposed 

10from viur.core.bones import * 

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

12from viur.core.prototypes.list import List 

13from viur.core.skeleton import Skeleton, ViurTagsSearchAdapter 

14 

15 

16class Creator(enum.Enum): 

17 VIUR = "viur" 

18 USER = "user" 

19 

20 

21class TranslationSkel(Skeleton): 

22 kindName = KINDNAME 

23 

24 database_adapters = [ 

25 ViurTagsSearchAdapter(max_length=256), 

26 ] 

27 

28 name = StringBone( 

29 descr=translate( 

30 "viur.core.translationskel.name.descr", 

31 "Translation key", 

32 ), 

33 searchable=True, 

34 escape_html=False, 

35 readOnly=True, # this is only readOnly=False on add! 

36 vfunc=lambda value: translate( 

37 "viur.core.translationskel.name.vfunc", 

38 "The translation key may not contain any upper-case characters." 

39 ) if any(ch.isupper() for ch in value) else None, 

40 unique=UniqueValue( 

41 UniqueLockMethod.SameValue, 

42 False, 

43 "This translation key already exists" 

44 ), 

45 ) 

46 

47 # FIXME: Remove with VIUR4 

48 tr_key = StringBone( 

49 descr="Translation key (OLD - DEPRECATED!)", 

50 escape_html=False, 

51 readOnly=True, 

52 visible=False, 

53 ) 

54 

55 translations = StringBone( 

56 descr=translate( 

57 "viur.core.translationskel.translations.descr", 

58 "Translations", 

59 ), 

60 searchable=True, 

61 languages=conf.i18n.available_dialects, 

62 escape_html=False, 

63 max_length=1024, 

64 params={ 

65 "tooltip": translate( 

66 "viur.core.translationskel.translations.tooltip", 

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

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

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

70 } 

71 ) 

72 

73 translations_missing = SelectBone( 

74 descr=translate( 

75 "viur.core.translationskel.translations_missing.descr", 

76 "Translation missing for language", 

77 ), 

78 multiple=True, 

79 readOnly=True, 

80 values=conf.i18n.available_dialects, 

81 compute=Compute( 

82 fn=lambda skel: [lang 

83 for lang in conf.i18n.available_dialects 

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

85 interval=ComputeInterval(ComputeMethod.OnWrite), 

86 ), 

87 ) 

88 

89 default_text = StringBone( 

90 descr=translate( 

91 "viur.core.translationskel.default_text.descr", 

92 "Fallback value", 

93 ), 

94 escape_html=False, 

95 ) 

96 

97 hint = StringBone( 

98 descr=translate( 

99 "viur.core.translationskel.hint.descr", 

100 "Hint / Context (internal only)", 

101 ), 

102 ) 

103 

104 usage_filename = StringBone( 

105 descr=translate( 

106 "viur.core.translationskel.usage_filename.descr", 

107 "Used and added from this file", 

108 ), 

109 readOnly=True, 

110 ) 

111 

112 usage_lineno = NumericBone( 

113 descr=translate( 

114 "viur.core.translationskel.usage_lineno.descr", 

115 "Used and added from this lineno", 

116 ), 

117 readOnly=True, 

118 ) 

119 

120 usage_variables = StringBone( 

121 descr=translate( 

122 "viur.core.translationskel.usage_variables.descr", 

123 "Receives these substitution variables", 

124 ), 

125 readOnly=True, 

126 multiple=True, 

127 ) 

128 

129 creator = SelectBone( 

130 descr=translate( 

131 "viur.core.translationskel.creator.descr", 

132 "Creator", 

133 ), 

134 readOnly=True, 

135 values=Creator, 

136 defaultValue=Creator.USER, 

137 ) 

138 

139 public = BooleanBone( 

140 descr=translate( 

141 "viur.core.translationskel.public.descr", 

142 "Is this translation public?", 

143 ), 

144 defaultValue=False, 

145 ) 

146 

147 @classmethod 

148 def read(cls, skel, *args, **kwargs): 

149 if skel := super().read(skel, *args, **kwargs): 

150 if skel["tr_key"]: 

151 skel["name"] = skel["tr_key"] 

152 skel["tr_key"] = None 

153 

154 return skel 

155 

156 @classmethod 

157 def write(cls, skel, **kwargs): 

158 # Create the key from the name on initial write! 

159 if not skel["key"]: 

160 skel["key"] = db.Key(KINDNAME, skel["name"]) 

161 

162 return super().write(skel, **kwargs) 

163 

164 

165class Translation(List): 

166 """ 

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

168 """ 

169 

170 kindName = KINDNAME 

171 

172 def adminInfo(self): 

173 return { 

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

175 "icon": "translate", 

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

177 "views": [ 

178 { 

179 "name": translate( 

180 "viur.core.translations.view.system", 

181 "ViUR System translations", 

182 ), 

183 "filter": { 

184 "name$lk": "viur.", 

185 } 

186 }, 

187 { 

188 "name": translate( 

189 "viur.core.translations.view.public", 

190 "Public translations", 

191 ), 

192 "filter": { 

193 "public": True, 

194 } 

195 } 

196 ] + [ 

197 { 

198 "name": translate( 

199 "viur.core.translations.view.missing", 

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

201 )(lang=lang), 

202 "filter": { 

203 "translations_missing": lang, 

204 }, 

205 } 

206 for lang in conf.i18n.available_dialects 

207 ], 

208 } 

209 

210 roles = { 

211 "admin": "*", 

212 } 

213 

214 def addSkel(self): 

215 """ 

216 Returns a custom TranslationSkel where the name is editable. 

217 The name becomes part of the key. 

218 """ 

219 skel = super().addSkel().ensure_is_cloned() 

220 skel.name.readOnly = False 

221 skel.name.required = True 

222 return skel 

223 

224 cloneSkel = addSkel 

225 

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

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

228 self._reload_translations() 

229 

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

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

232 self._reload_translations() 

233 

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

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

236 self._reload_translations() 

237 

238 def _reload_translations(self): 

239 if ( 

240 self._last_reload is not None 

241 and self._last_reload - utils.utcNow() < datetime.timedelta(minutes=10) 

242 ): 

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

244 return None 

245 

246 logging.info("Reload translations") 

247 # TODO: this affects only the current instance 

248 self._last_reload = utils.utcNow() 

249 systemTranslations.clear() 

250 initializeTranslations() 

251 

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

253 

254 @exposed 

255 def dump( 

256 self, 

257 *, 

258 pattern: list[str] | None = None, 

259 language: list[str] | None = None, 

260 ) -> dict[str, dict[str, str]]: 

261 """ 

262 Dumps translations as JSON. 

263 

264 :param pattern: Optional, provide fnmatch-style translation key filter patterns of the translations wanted. 

265 :param language: Allows to request a specific language. 

266 By default, the language of the current request is used. 

267 

268 :return: A dictionary with translations as JSON. Structure: ``{language: {key: value, ...}, ...}`` 

269 

270 Example calls: 

271 

272 - `/json/_translation/dump?pattern=viur.*` get viur.*-translations for current language 

273 - `/json/_translation/dump?pattern=viur.*&language=en` for english translations 

274 - `/json/_translation/dump?pattern=viur.*&language=en&language=de` for english and german translations 

275 - `/json/_translation/dump?pattern=viur.*&language=*` for all available language 

276 """ 

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

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

279 

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

281 

282 if ( 

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

284 and any(os.getenv("HTTP_HOST", "") in dlm for dlm in conf.i18n.domain_language_mapping) 

285 ): 

286 # cache it 7 days 

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

288 

289 if language: 

290 if len(language) == 1 and language[0] == "*": 

291 language = conf.i18n.available_dialects 

292 else: 

293 language = [current.language.get()] 

294 

295 return json.dumps({ # type: ignore 

296 lang: { 

297 name: str(translate(name, force_lang=lang)) 

298 for name, values in systemTranslations.items() 

299 if (conf.i18n.dump_can_view(name) or values.get("_public_")) 

300 and (not pattern or any(fnmatch.fnmatch(name, pat) for pat in pattern)) 

301 } 

302 for lang in language 

303 }) 

304 

305 @exposed 

306 @deprecated( 

307 version="3.7.10", 

308 reason="Function renamed. Use 'dump' function as alternative implementation.", 

309 ) 

310 def get_public(self, *, languages: list[str] = [], **kwargs): 

311 return self.dump(language=languages, **kwargs) 

312 

313 

314Translation.json = True