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
« 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
15class Creator(enum.Enum):
16 VIUR = "viur"
17 USER = "user"
20class TranslationSkel(Skeleton):
21 kindName = KINDNAME
23 database_adapters = [
24 ViurTagsSearchAdapter(max_length=256),
25 ]
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 )
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 )
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 )
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 )
83 default_text = StringBone(
84 escape_html=False,
85 descr=translate(
86 "core.translationskel.default_text.descr",
87 "Fallback value",
88 ),
89 )
91 hint = StringBone(
92 descr=translate(
93 "core.translationskel.hint.descr",
94 "Hint / Context (internal only)",
95 ),
96 )
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 )
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 )
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 )
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 )
133 public = BooleanBone(
134 descr=translate(
135 "core.translationskel.public.descr",
136 "Is this translation public?",
137 ),
138 defaultValue=False,
139 )
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)
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)
155class Translation(List):
156 """
157 The Translation module is a system module used by the ViUR framework for its internationalization capabilities.
158 """
160 kindName = KINDNAME
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 }
181 roles = {
182 "admin": "*",
183 }
185 def onAdded(self, *args, **kwargs):
186 super().onAdded(*args, **kwargs)
187 self._reload_translations()
189 def onEdited(self, *args, **kwargs):
190 super().onEdited(*args, **kwargs)
191 self._reload_translations()
193 def onDeleted(self, *args, **kwargs):
194 super().onDeleted(*args, **kwargs)
195 self._reload_translations()
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()
210 _last_reload = None # Cut my strings into pieces, this is my last reload...
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.
222 :param languages: Allows to request a specific language.
223 :param pattern: Provide an fnmatch-style key filter pattern
225 Example calls:
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")
237 current.request.get().response.headers["Content-Type"] = "application/json"
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}"
246 if languages:
247 if len(languages) == 1 and languages[0] == "*":
248 languages = conf.i18n.available_dialects
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 })
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 })
266Translation.json = True