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
« 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
15class Creator(enum.Enum):
16 VIUR = "viur"
17 USER = "user"
20class TranslationSkel(Skeleton):
21 kindName = KINDNAME
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 )
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 )
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 )
65 default_text = StringBone(
66 descr=translate(
67 "core.translationskel.default_text.descr",
68 "Fallback value",
69 ),
70 )
72 hint = StringBone(
73 descr=translate(
74 "core.translationskel.hint.descr",
75 "Hint / Context (internal only)",
76 ),
77 )
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 )
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 )
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 )
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 )
114 public = BooleanBone(
115 descr=translate(
116 "core.translationskel.public.descr",
117 "Is this translation public?",
118 ),
119 defaultValue=False,
120 )
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)
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)
136class Translation(List):
137 """
138 The Translation module is a system module used by the ViUR framework for its internationalization capabilities.
139 """
141 kindName = KINDNAME
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 }
162 roles = {
163 "admin": "*",
164 }
166 def onAdded(self, *args, **kwargs):
167 super().onAdded(*args, **kwargs)
168 self._reload_translations()
170 def onEdited(self, *args, **kwargs):
171 super().onEdited(*args, **kwargs)
172 self._reload_translations()
174 def onDeleted(self, *args, **kwargs):
175 super().onDeleted(*args, **kwargs)
176 self._reload_translations()
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()
191 _last_reload = None # Cut my strings into pieces, this is my last reload...
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.
203 :param languages: Allows to request a specific language.
204 :param pattern: Provide an fnmatch-style key filter pattern
206 Example calls:
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")
218 current.request.get().response.headers["Content-Type"] = "application/json"
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}"
227 if languages:
228 if len(languages) == 1 and languages[0] == "*":
229 languages = conf.i18n.available_dialects
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 })
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 })
247Translation.json = True