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

70 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-02-07 19:28 +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 

13 

14 

15class Creator(enum.Enum): 

16 VIUR = "viur" 

17 USER = "user" 

18 

19 

20class TranslationSkel(Skeleton): 

21 kindName = KINDNAME 

22 

23 tr_key = StringBone( 

24 descr=translate( 

25 "core.translationskel.tr_key.descr", 

26 "Translation key", 

27 ), 

28 searchable=True, 

29 unique=UniqueValue(UniqueLockMethod.SameValue, False, 

30 "This translation key exist already"), 

31 ) 

32 

33 translations = StringBone( 

34 descr=translate( 

35 "core.translationskel.translations.descr", 

36 "Translations", 

37 ), 

38 searchable=True, 

39 languages=conf.i18n.available_dialects, 

40 params={ 

41 "tooltip": translate( 

42 "core.translationskel.translations.tooltip", 

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

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

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

46 } 

47 ) 

48 

49 translations_missing = SelectBone( 

50 descr=translate( 

51 "core.translationskel.translations_missing.descr", 

52 "Translation missing for language", 

53 ), 

54 multiple=True, 

55 readOnly=True, 

56 values=conf.i18n.available_dialects, 

57 compute=Compute( 

58 fn=lambda skel: [lang 

59 for lang in conf.i18n.available_dialects 

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

61 interval=ComputeInterval(ComputeMethod.OnWrite), 

62 ), 

63 ) 

64 

65 default_text = StringBone( 

66 descr=translate( 

67 "core.translationskel.default_text.descr", 

68 "Fallback value", 

69 ), 

70 ) 

71 

72 hint = StringBone( 

73 descr=translate( 

74 "core.translationskel.hint.descr", 

75 "Hint / Context (internal only)", 

76 ), 

77 ) 

78 

79 usage_filename = StringBone( 

80 descr=translate( 

81 "core.translationskel.usage_filename.descr", 

82 "Used and added from this file", 

83 ), 

84 readOnly=True, 

85 ) 

86 

87 usage_lineno = NumericBone( 

88 descr=translate( 

89 "core.translationskel.usage_lineno.descr", 

90 "Used and added from this lineno", 

91 ), 

92 readOnly=True, 

93 ) 

94 

95 usage_variables = StringBone( 

96 descr=translate( 

97 "core.translationskel.usage_variables.descr", 

98 "Receives these substitution variables", 

99 ), 

100 readOnly=True, 

101 multiple=True, 

102 ) 

103 

104 creator = SelectBone( 

105 descr=translate( 

106 "core.translationskel.creator.descr", 

107 "Creator", 

108 ), 

109 readOnly=True, 

110 values=Creator, 

111 defaultValue=Creator.USER, 

112 ) 

113 

114 public = BooleanBone( 

115 descr=translate( 

116 "core.translationskel.public.descr", 

117 "Is this translation public?", 

118 ), 

119 defaultValue=False, 

120 ) 

121 

122 @classmethod 

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

124 # Ensure we have only lowercase keys 

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

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

127 

128 @classmethod 

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

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

131 # TODO: Remove in ViUR4 

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

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

134 

135 

136class Translation(List): 

137 """ 

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

139 """ 

140 

141 kindName = KINDNAME 

142 

143 def adminInfo(self): 

144 return { 

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

146 "icon": "translate", 

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

148 "views": [ 

149 { 

150 "name": translate( 

151 "core.translations.view.missing", 

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

153 )(lang=lang), 

154 "filter": { 

155 "translations_missing": lang, 

156 }, 

157 } 

158 for lang in conf.i18n.available_dialects 

159 ], 

160 } 

161 

162 roles = { 

163 "admin": "*", 

164 } 

165 

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

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

168 self._reload_translations() 

169 

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

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

172 self._reload_translations() 

173 

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

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

176 self._reload_translations() 

177 

178 def _reload_translations(self): 

179 if ( 

180 self._last_reload is not None 

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

182 ): 

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

184 return None 

185 logging.info("Reload translations") 

186 # TODO: this affects only the current instance 

187 self._last_reload = utils.utcNow() 

188 systemTranslations.clear() 

189 initializeTranslations() 

190 

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

192 

193 @exposed 

194 def get_public( 

195 self, 

196 *, 

197 languages: list[str] = [], 

198 pattern: str = "*", 

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

200 """ 

201 Dumps public translations as JSON. 

202 

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

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

205 

206 Example calls: 

207 

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

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

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

211 but only keys starting with "bool." 

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

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

214 """ 

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

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

217 

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

219 

220 if ( 

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

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

223 ): 

224 # cache it 7 days 

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

226 

227 if languages: 

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

229 languages = conf.i18n.available_dialects 

230 

231 return json.dumps({ 

232 lang: { 

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

234 for tr_key, values in systemTranslations.items() 

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

236 } 

237 for lang in languages 

238 }) 

239 

240 return json.dumps({ 

241 tr_key: str(translate(tr_key)) 

242 for tr_key, values in systemTranslations.items() 

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

244 }) 

245 

246 

247Translation.json = True