Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Public translations interface #1323

Open
wants to merge 3 commits into
base: 3.6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 41 additions & 6 deletions src/viur/core/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,23 @@ class translate:
translation issues with bones, which can now take an instance of this class as it's description/hints.
"""

__slots__ = ["key", "defaultText", "hint", "translationCache", "force_lang"]

def __init__(self, key: str, defaultText: str = None, hint: str = None, force_lang: str = None):
__slots__ = (
"key",
"defaultText",
"hint",
"translationCache",
"force_lang",
"public",
)

def __init__(
self,
key: str,
defaultText: str = None,
hint: str = None,
force_lang: str = None,
public: bool = False,
):
"""
:param key: The unique key defining this text fragment.
Usually it's a path/filename and a unique descriptor in that file
Expand All @@ -163,16 +177,20 @@ def __init__(self, key: str, defaultText: str = None, hint: str = None, force_la
as the key/defaultText may have different meanings in the
target language.
:param force_lang: Use this language instead the one of the request.
:param public: Flag for public translations, which can be obtained via /json/_translate/get_public.
"""
super().__init__()
key = str(key) # ensure key is a str
self.key = key.lower()

self.key = str(key).lower()
self.defaultText = defaultText or key
self.hint = hint

self.translationCache = None
if force_lang is not None and force_lang not in conf.i18n.available_dialects:
raise ValueError(f"The language {force_lang=} is not available")

self.force_lang = force_lang
self.public = public

def __repr__(self) -> str:
return f"<translate object for {self.key} with force_lang={self.force_lang}>"
Expand Down Expand Up @@ -211,6 +229,7 @@ def __str__(self) -> str:
default_text=self.defaultText,
filename=filename,
lineno=lineno,
public=self.public,
)

self.translationCache = self.merge_alias(systemTranslations.get(self.key, {}))
Expand Down Expand Up @@ -272,15 +291,19 @@ class TranslationExtension(jinja2.Extension):
force the use of a specific language, not the language of the request.
"""

tags = {"translate"}
tags = {
"translate",
}

def parse(self, parser):
# Parse the translate tag
global systemTranslations

args = [] # positional args for the `_translate()` method
kwargs = {} # keyword args (force_lang + substitute vars) for the `_translate()` method
lineno = parser.stream.current.lineno
filename = parser.stream.filename

# Parse arguments (args and kwargs) until the current block ends
lastToken = None
while parser.stream.current.type != 'block_end':
Expand All @@ -301,14 +324,19 @@ def parse(self, parser):
else:
raise SyntaxError()
lastToken = None

if lastToken: # TODO: what's this? what it is doing?
logging.debug(f"final append {lastToken = }")
args.append(lastToken.value)

if not 0 < len(args) <= 3:
raise SyntaxError("Translation-Key missing or excess parameters!")

args += [""] * (3 - len(args))
args += [kwargs]
tr_key = args[0].lower()
public = kwargs.pop("_public_", False) or False

if tr_key not in systemTranslations:
add_missing_translation(
key=tr_key,
Expand All @@ -317,6 +345,7 @@ def parse(self, parser):
filename=filename,
lineno=lineno,
variables=list(kwargs.keys()),
public=public,
)

translations = translate.merge_alias(systemTranslations.get(tr_key, {}))
Expand Down Expand Up @@ -369,7 +398,9 @@ def initializeTranslations() -> None:

translations = {
"_default_text_": entity.get("default_text") or None,
"_public_": entity.get("public") or False,
}

for lang, translation in entity["translations"].items():
if lang not in conf.i18n.available_dialects:
# Don't store unknown languages in the memory
Expand All @@ -378,6 +409,7 @@ def initializeTranslations() -> None:
# Skip empty values
continue
translations[lang] = translation

systemTranslations[entity["tr_key"]] = translations


Expand All @@ -390,6 +422,7 @@ def add_missing_translation(
filename: str | None = None,
lineno: int | None = None,
variables: list[str] = None,
public: bool = False,
) -> None:
"""Add missing translations to datastore"""
try:
Expand Down Expand Up @@ -426,11 +459,13 @@ def add_missing_translation(
skel["usage_lineno"] = lineno
skel["usage_variables"] = variables or []
skel["creator"] = Creator.VIUR
skel["public"] = public
skel.toDB()

# Add to system translation to avoid triggering this method again
systemTranslations[key] = {
"_default_text_": default_text or None,
"_public_": public,
}


Expand Down
62 changes: 59 additions & 3 deletions src/viur/core/modules/translation.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import enum
import json
import logging
import os
from datetime import timedelta as td

from viur.core import conf, db, utils
from viur.core import conf, db, utils, current, errors
from viur.core.decorators import exposed
from viur.core.bones import *
from viur.core.i18n import KINDNAME, initializeTranslations, systemTranslations, translate
from viur.core.prototypes.list import List
Expand Down Expand Up @@ -108,6 +110,14 @@ class TranslationSkel(Skeleton):
defaultValue=Creator.USER,
)

public = BooleanBone(
descr=translate(
"core.translationskel.public.descr",
"Is this translation public?",
),
defaultValue=False,
)

@classmethod
def toDB(cls, skelValues: SkeletonInstance, **kwargs) -> db.Key:
# Ensure we have only lowercase keys
Expand Down Expand Up @@ -173,4 +183,50 @@ def _reload_translations(self):
systemTranslations.clear()
initializeTranslations()

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

@exposed
def get_public(self, *, languages: list[str] = None) -> dict[str, str] | dict[str, dict[str, str]]:
"""
Dumps public translations as JSON.

Example calls:

- `/json/_translation/get_public` get public translations for current language
- `/json/_translation/get_public?languages=en` for english translations
- `/json/_translation/get_public?languages=en&languages=de` for english and german translations
- `/json/_translation/get_public?languages=*` for all available languages
"""
if not utils.string.is_prefix(self.render.kind, "json"):
raise errors.BadRequest("Can only use this function on JSON-based renders")

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

if (
not (conf.debug.disable_cache and current.request.get().disableCache)
and any(os.getenv("HTTP_HOST", "") in x for x in conf.i18n.domain_language_mapping)
):
# cache it 7 days
current.request.get().response.headers["Cache-Control"] = f"public, max-age={7 * 24 * 60 * 60}"

if languages:
if len(languages) == 1 and languages[0] == "*":
languages = conf.i18n.available_dialects

return json.dumps({
lang: {
tr_key: str(translate(tr_key, force_lang=lang))
for tr_key, values in systemTranslations.items()
if values.get("_public_")
}
for lang in languages
})

return json.dumps({
tr_key: str(translate(tr_key))
for tr_key, values in systemTranslations.items()
if values.get("_public_")
})


Translation.json = True
Loading