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
« 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
16class Creator(enum.Enum):
17 VIUR = "viur"
18 USER = "user"
21class TranslationSkel(Skeleton):
22 kindName = KINDNAME
24 database_adapters = [
25 ViurTagsSearchAdapter(max_length=256),
26 ]
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 )
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 )
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 )
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 )
89 default_text = StringBone(
90 descr=translate(
91 "viur.core.translationskel.default_text.descr",
92 "Fallback value",
93 ),
94 escape_html=False,
95 )
97 hint = StringBone(
98 descr=translate(
99 "viur.core.translationskel.hint.descr",
100 "Hint / Context (internal only)",
101 ),
102 )
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 )
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 )
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 )
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 )
139 public = BooleanBone(
140 descr=translate(
141 "viur.core.translationskel.public.descr",
142 "Is this translation public?",
143 ),
144 defaultValue=False,
145 )
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
154 return skel
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"])
162 return super().write(skel, **kwargs)
165class Translation(List):
166 """
167 The Translation module is a system module used by the ViUR framework for its internationalization capabilities.
168 """
170 kindName = KINDNAME
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 }
210 roles = {
211 "admin": "*",
212 }
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
224 cloneSkel = addSkel
226 def onAdded(self, *args, **kwargs):
227 super().onAdded(*args, **kwargs)
228 self._reload_translations()
230 def onEdited(self, *args, **kwargs):
231 super().onEdited(*args, **kwargs)
232 self._reload_translations()
234 def onDeleted(self, *args, **kwargs):
235 super().onDeleted(*args, **kwargs)
236 self._reload_translations()
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
246 logging.info("Reload translations")
247 # TODO: this affects only the current instance
248 self._last_reload = utils.utcNow()
249 systemTranslations.clear()
250 initializeTranslations()
252 _last_reload = None # Cut my strings into pieces, this is my last reload...
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.
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.
268 :return: A dictionary with translations as JSON. Structure: ``{language: {key: value, ...}, ...}``
270 Example calls:
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")
280 current.request.get().response.headers["Content-Type"] = "application/json"
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}"
289 if language:
290 if len(language) == 1 and language[0] == "*":
291 language = conf.i18n.available_dialects
292 else:
293 language = [current.language.get()]
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 })
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)
314Translation.json = True