Commit 93bac8ec authored by Kevin Morris's avatar Kevin Morris Committed by Lukas Fleischer
Browse files

aurweb: Globalize a Translator instance, add more utility



+ Added SUPPORTED_LANGUAGES, a global constant dictionary of
  language => display pairs for languages we support.
+ Add Translator.get_translator, a function used to retrieve a
  translator after initializing it (if needed). Use `fallback=True`
  while creating languages, in case we setup a language that we
  don't have a translation for, it will noop the translation.
  This is particularly useful for "en," since we do not translate
  it, but doing this will allow us to go through our normal translation
  flow in any case.
+ Added typing.
+ Added get_request_language, a function that grabs the language for
  a request session, defaulting to aurweb.config [options] default_lang.
+ Added get_raw_translator_for_request, a function that retrieves
  the concrete translation object for a given language.
+ Added tr, a jinja2 contextfilter that can be used to inline translate
  strings in jinja2 templates.
+ Added `python-jinja` dep to .gitlab-ci.yml. This needs to be
  included in documentation before this set is merged in.
+ Introduce pytest units (test_l10n.py) in `test` along with
  __init__.py, which marks `test` as a test package.
+ Additionally, fix up notify.py to use the global translator. Also
  reduce its source width to <= 80 by newlining some code.
+ Additionally, prepare locale in .gitlab-ci.yml and add
  aurweb.config [options] localedir to config.dev with YOUR_AUR_ROOT
  like others.
Signed-off-by: Kevin Morris's avatarKevin Morris <kevr@0cost.org>
Signed-off-by: Lukas Fleischer's avatarLukas Fleischer <lfleischer@archlinux.org>
parent 3bd4aa4c
...@@ -20,6 +20,7 @@ test: ...@@ -20,6 +20,7 @@ test:
script: script:
- python setup.py install - python setup.py install
- sed -r "s;YOUR_AUR_ROOT;$(pwd);g" conf/config.dev > conf/config - sed -r "s;YOUR_AUR_ROOT;$(pwd);g" conf/config.dev > conf/config
- AUR_CONFIG=conf/config make -C po all install
- AUR_CONFIG=conf/config python -m aurweb.initdb - AUR_CONFIG=conf/config python -m aurweb.initdb
- make -C test - make -C test
- coverage report --include='aurweb/*' - coverage report --include='aurweb/*'
import gettext import gettext
import typing
from collections import OrderedDict
from fastapi import Request
from jinja2 import contextfilter
import aurweb.config import aurweb.config
SUPPORTED_LANGUAGES = OrderedDict({
"ar": "العربية",
"ast": "Asturianu",
"ca": "Català",
"cs": "Český",
"da": "Dansk",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"es_419": "Español (Latinoamérica)",
"fi": "Suomi",
"fr": "Français",
"he": "עברית",
"hr": "Hrvatski",
"hu": "Magyar",
"it": "Italiano",
"ja": "日本語",
"nb": "Norsk",
"nl": "Nederlands",
"pl": "Polski",
"pt_BR": "Português (Brasil)",
"pt_PT": "Português (Portugal)",
"ro": "Română",
"ru": "Русский",
"sk": "Slovenčina",
"sr": "Srpski",
"tr": "Türkçe",
"uk": "Українська",
"zh_CN": "简体中文",
"zh_TW": "正體中文"
})
class Translator: class Translator:
def __init__(self): def __init__(self):
self._localedir = aurweb.config.get('options', 'localedir') self._localedir = aurweb.config.get('options', 'localedir')
self._translator = {} self._translator = {}
def translate(self, s, lang): def get_translator(self, lang: str):
if lang == 'en':
return s
if lang not in self._translator: if lang not in self._translator:
self._translator[lang] = gettext.translation("aurweb", self._translator[lang] = gettext.translation("aurweb",
self._localedir, self._localedir,
languages=[lang]) languages=[lang],
return self._translator[lang].gettext(s) fallback=True)
return self._translator.get(lang)
def translate(self, s: str, lang: str):
return self.get_translator(lang).gettext(s)
# Global translator object.
translator = Translator()
def get_translator_for_request(request): def get_request_language(request: Request):
return request.cookies.get("AURLANG",
aurweb.config.get("options", "default_lang"))
def get_raw_translator_for_request(request: Request):
lang = get_request_language(request)
return translator.get_translator(lang)
def get_translator_for_request(request: Request):
""" """
Determine the preferred language from a FastAPI request object and build a Determine the preferred language from a FastAPI request object and build a
translator function for it. translator function for it.
...@@ -29,12 +84,16 @@ def get_translator_for_request(request): ...@@ -29,12 +84,16 @@ def get_translator_for_request(request):
print(_("Hello")) print(_("Hello"))
``` ```
""" """
lang = request.cookies.get("AURLANG") lang = get_request_language(request)
if lang is None:
lang = aurweb.config.get("options", "default_lang")
translator = Translator()
def translate(message): def translate(message):
return translator.translate(message, lang) return translator.translate(message, lang)
return translate return translate
@contextfilter
def tr(context: typing.Any, value: str):
""" A translation filter; example: {{ "Hello" | tr("de") }}. """
_ = get_translator_for_request(context.get("request"))
return _(value)
...@@ -40,9 +40,6 @@ def pkgbase_from_pkgreq(conn, reqid): ...@@ -40,9 +40,6 @@ def pkgbase_from_pkgreq(conn, reqid):
class Notification: class Notification:
def __init__(self):
self._l10n = aurweb.l10n.Translator()
def get_refs(self): def get_refs(self):
return () return ()
...@@ -97,9 +94,12 @@ class Notification: ...@@ -97,9 +94,12 @@ class Notification:
else: else:
# send email using smtplib; no local MTA required # send email using smtplib; no local MTA required
server_addr = aurweb.config.get('notifications', 'smtp-server') server_addr = aurweb.config.get('notifications', 'smtp-server')
server_port = aurweb.config.getint('notifications', 'smtp-port') server_port = aurweb.config.getint('notifications',
use_ssl = aurweb.config.getboolean('notifications', 'smtp-use-ssl') 'smtp-port')
use_starttls = aurweb.config.getboolean('notifications', 'smtp-use-starttls') use_ssl = aurweb.config.getboolean('notifications',
'smtp-use-ssl')
use_starttls = aurweb.config.getboolean('notifications',
'smtp-use-starttls')
user = aurweb.config.get('notifications', 'smtp-user') user = aurweb.config.get('notifications', 'smtp-user')
passwd = aurweb.config.get('notifications', 'smtp-password') passwd = aurweb.config.get('notifications', 'smtp-password')
...@@ -127,7 +127,8 @@ class ResetKeyNotification(Notification): ...@@ -127,7 +127,8 @@ class ResetKeyNotification(Notification):
cur = conn.execute('SELECT UserName, Email, BackupEmail, ' + cur = conn.execute('SELECT UserName, Email, BackupEmail, ' +
'LangPreference, ResetKey ' + 'LangPreference, ResetKey ' +
'FROM Users WHERE ID = ? AND Suspended = 0', [uid]) 'FROM Users WHERE ID = ? AND Suspended = 0', [uid])
self._username, self._to, self._backup, self._lang, self._resetkey = cur.fetchone() self._username, self._to, self._backup, self._lang, self._resetkey = \
cur.fetchone()
super().__init__() super().__init__()
def get_recipients(self): def get_recipients(self):
...@@ -137,10 +138,10 @@ class ResetKeyNotification(Notification): ...@@ -137,10 +138,10 @@ class ResetKeyNotification(Notification):
return [(self._to, self._lang)] return [(self._to, self._lang)]
def get_subject(self, lang): def get_subject(self, lang):
return self._l10n.translate('AUR Password Reset', lang) return aurweb.l10n.translator.translate('AUR Password Reset', lang)
def get_body(self, lang): def get_body(self, lang):
return self._l10n.translate( return aurweb.l10n.translator.translate(
'A password reset request was submitted for the account ' 'A password reset request was submitted for the account '
'{user} associated with your email address. If you wish to ' '{user} associated with your email address. If you wish to '
'reset your password follow the link [1] below, otherwise ' 'reset your password follow the link [1] below, otherwise '
...@@ -153,11 +154,12 @@ class ResetKeyNotification(Notification): ...@@ -153,11 +154,12 @@ class ResetKeyNotification(Notification):
class WelcomeNotification(ResetKeyNotification): class WelcomeNotification(ResetKeyNotification):
def get_subject(self, lang): def get_subject(self, lang):
return self._l10n.translate('Welcome to the Arch User Repository', return aurweb.l10n.translator.translate(
'Welcome to the Arch User Repository',
lang) lang)
def get_body(self, lang): def get_body(self, lang):
return self._l10n.translate( return aurweb.l10n.translator.translate(
'Welcome to the Arch User Repository! In order to set an ' 'Welcome to the Arch User Repository! In order to set an '
'initial password for your new account, please click the ' 'initial password for your new account, please click the '
'link [1] below. If the link does not work, try copying and ' 'link [1] below. If the link does not work, try copying and '
...@@ -186,16 +188,18 @@ class CommentNotification(Notification): ...@@ -186,16 +188,18 @@ class CommentNotification(Notification):
return self._recipients return self._recipients
def get_subject(self, lang): def get_subject(self, lang):
return self._l10n.translate('AUR Comment for {pkgbase}', return aurweb.l10n.translator.translate(
'AUR Comment for {pkgbase}',
lang).format(pkgbase=self._pkgbase) lang).format(pkgbase=self._pkgbase)
def get_body(self, lang): def get_body(self, lang):
body = self._l10n.translate( body = aurweb.l10n.translator.translate(
'{user} [1] added the following comment to {pkgbase} [2]:', '{user} [1] added the following comment to {pkgbase} [2]:',
lang).format(user=self._user, pkgbase=self._pkgbase) lang).format(user=self._user, pkgbase=self._pkgbase)
body += '\n\n' + self._text + '\n\n-- \n' body += '\n\n' + self._text + '\n\n-- \n'
dnlabel = self._l10n.translate('Disable notifications', lang) dnlabel = aurweb.l10n.translator.translate(
body += self._l10n.translate( 'Disable notifications', lang)
body += aurweb.l10n.translator.translate(
'If you no longer wish to receive notifications about this ' 'If you no longer wish to receive notifications about this '
'package, please go to the package page [2] and select ' 'package, please go to the package page [2] and select '
'"{label}".', lang).format(label=dnlabel) '"{label}".', lang).format(label=dnlabel)
...@@ -231,17 +235,18 @@ class UpdateNotification(Notification): ...@@ -231,17 +235,18 @@ class UpdateNotification(Notification):
return self._recipients return self._recipients
def get_subject(self, lang): def get_subject(self, lang):
return self._l10n.translate('AUR Package Update: {pkgbase}', return aurweb.l10n.translator.translate(
'AUR Package Update: {pkgbase}',
lang).format(pkgbase=self._pkgbase) lang).format(pkgbase=self._pkgbase)
def get_body(self, lang): def get_body(self, lang):
body = self._l10n.translate('{user} [1] pushed a new commit to ' body = aurweb.l10n.translator.translate(
'{pkgbase} [2].', lang).format( '{user} [1] pushed a new commit to {pkgbase} [2].',
user=self._user, lang).format(user=self._user, pkgbase=self._pkgbase)
pkgbase=self._pkgbase)
body += '\n\n-- \n' body += '\n\n-- \n'
dnlabel = self._l10n.translate('Disable notifications', lang) dnlabel = aurweb.l10n.translator.translate(
body += self._l10n.translate( 'Disable notifications', lang)
body += aurweb.l10n.translator.translate(
'If you no longer wish to receive notifications about this ' 'If you no longer wish to receive notifications about this '
'package, please go to the package page [2] and select ' 'package, please go to the package page [2] and select '
'"{label}".', lang).format(label=dnlabel) '"{label}".', lang).format(label=dnlabel)
...@@ -261,7 +266,8 @@ class FlagNotification(Notification): ...@@ -261,7 +266,8 @@ class FlagNotification(Notification):
def __init__(self, conn, uid, pkgbase_id): def __init__(self, conn, uid, pkgbase_id):
self._user = username_from_id(conn, uid) self._user = username_from_id(conn, uid)
self._pkgbase = pkgbase_from_id(conn, pkgbase_id) self._pkgbase = pkgbase_from_id(conn, pkgbase_id)
cur = conn.execute('SELECT DISTINCT Users.Email, ' + cur = conn.execute(
'SELECT DISTINCT Users.Email, ' +
'Users.LangPreference FROM Users ' + 'Users.LangPreference FROM Users ' +
'LEFT JOIN PackageComaintainers ' + 'LEFT JOIN PackageComaintainers ' +
'ON PackageComaintainers.UsersID = Users.ID ' + 'ON PackageComaintainers.UsersID = Users.ID ' +
...@@ -280,12 +286,12 @@ class FlagNotification(Notification): ...@@ -280,12 +286,12 @@ class FlagNotification(Notification):
return self._recipients return self._recipients
def get_subject(self, lang): def get_subject(self, lang):
return self._l10n.translate('AUR Out-of-date Notification for ' return aurweb.l10n.translator.translate(
'{pkgbase}', 'AUR Out-of-date Notification for {pkgbase}',
lang).format(pkgbase=self._pkgbase) lang).format(pkgbase=self._pkgbase)
def get_body(self, lang): def get_body(self, lang):
body = self._l10n.translate( body = aurweb.l10n.translator.translate(
'Your package {pkgbase} [1] has been flagged out-of-date by ' 'Your package {pkgbase} [1] has been flagged out-of-date by '
'{user} [2]:', lang).format(pkgbase=self._pkgbase, '{user} [2]:', lang).format(pkgbase=self._pkgbase,
user=self._user) user=self._user)
...@@ -320,7 +326,8 @@ class OwnershipEventNotification(Notification): ...@@ -320,7 +326,8 @@ class OwnershipEventNotification(Notification):
return self._recipients return self._recipients
def get_subject(self, lang): def get_subject(self, lang):
return self._l10n.translate('AUR Ownership Notification for {pkgbase}', return aurweb.l10n.translator.translate(
'AUR Ownership Notification for {pkgbase}',
lang).format(pkgbase=self._pkgbase) lang).format(pkgbase=self._pkgbase)
def get_refs(self): def get_refs(self):
...@@ -330,14 +337,14 @@ class OwnershipEventNotification(Notification): ...@@ -330,14 +337,14 @@ class OwnershipEventNotification(Notification):
class AdoptNotification(OwnershipEventNotification): class AdoptNotification(OwnershipEventNotification):
def get_body(self, lang): def get_body(self, lang):
return self._l10n.translate( return aurweb.l10n.translator.translate(
'The package {pkgbase} [1] was adopted by {user} [2].', 'The package {pkgbase} [1] was adopted by {user} [2].',
lang).format(pkgbase=self._pkgbase, user=self._user) lang).format(pkgbase=self._pkgbase, user=self._user)
class DisownNotification(OwnershipEventNotification): class DisownNotification(OwnershipEventNotification):
def get_body(self, lang): def get_body(self, lang):
return self._l10n.translate( return aurweb.l10n.translator.translate(
'The package {pkgbase} [1] was disowned by {user} ' 'The package {pkgbase} [1] was disowned by {user} '
'[2].', lang).format(pkgbase=self._pkgbase, '[2].', lang).format(pkgbase=self._pkgbase,
user=self._user) user=self._user)
...@@ -355,8 +362,8 @@ class ComaintainershipEventNotification(Notification): ...@@ -355,8 +362,8 @@ class ComaintainershipEventNotification(Notification):
return [(self._to, self._lang)] return [(self._to, self._lang)]
def get_subject(self, lang): def get_subject(self, lang):
return self._l10n.translate('AUR Co-Maintainer Notification for ' return aurweb.l10n.translator.translate(
'{pkgbase}', 'AUR Co-Maintainer Notification for {pkgbase}',
lang).format(pkgbase=self._pkgbase) lang).format(pkgbase=self._pkgbase)
def get_refs(self): def get_refs(self):
...@@ -365,14 +372,14 @@ class ComaintainershipEventNotification(Notification): ...@@ -365,14 +372,14 @@ class ComaintainershipEventNotification(Notification):
class ComaintainerAddNotification(ComaintainershipEventNotification): class ComaintainerAddNotification(ComaintainershipEventNotification):
def get_body(self, lang): def get_body(self, lang):
return self._l10n.translate( return aurweb.l10n.translator.translate(
'You were added to the co-maintainer list of {pkgbase} [1].', 'You were added to the co-maintainer list of {pkgbase} [1].',
lang).format(pkgbase=self._pkgbase) lang).format(pkgbase=self._pkgbase)
class ComaintainerRemoveNotification(ComaintainershipEventNotification): class ComaintainerRemoveNotification(ComaintainershipEventNotification):
def get_body(self, lang): def get_body(self, lang):
return self._l10n.translate( return aurweb.l10n.translator.translate(
'You were removed from the co-maintainer list of {pkgbase} ' 'You were removed from the co-maintainer list of {pkgbase} '
'[1].', lang).format(pkgbase=self._pkgbase) '[1].', lang).format(pkgbase=self._pkgbase)
...@@ -400,13 +407,15 @@ class DeleteNotification(Notification): ...@@ -400,13 +407,15 @@ class DeleteNotification(Notification):
return self._recipients return self._recipients
def get_subject(self, lang): def get_subject(self, lang):
return self._l10n.translate('AUR Package deleted: {pkgbase}', return aurweb.l10n.translator.translate(
'AUR Package deleted: {pkgbase}',
lang).format(pkgbase=self._old_pkgbase) lang).format(pkgbase=self._old_pkgbase)
def get_body(self, lang): def get_body(self, lang):
if self._new_pkgbase: if self._new_pkgbase:
dnlabel = self._l10n.translate('Disable notifications', lang) dnlabel = aurweb.l10n.translator.translate(
return self._l10n.translate( 'Disable notifications', lang)
return aurweb.l10n.translator.translate(
'{user} [1] merged {old} [2] into {new} [3].\n\n' '{user} [1] merged {old} [2] into {new} [3].\n\n'
'-- \n' '-- \n'
'If you no longer wish receive notifications about the ' 'If you no longer wish receive notifications about the '
...@@ -414,7 +423,7 @@ class DeleteNotification(Notification): ...@@ -414,7 +423,7 @@ class DeleteNotification(Notification):
lang).format(user=self._user, old=self._old_pkgbase, lang).format(user=self._user, old=self._old_pkgbase,
new=self._new_pkgbase, label=dnlabel) new=self._new_pkgbase, label=dnlabel)
else: else:
return self._l10n.translate( return aurweb.l10n.translator.translate(
'{user} [1] deleted {pkgbase} [2].\n\n' '{user} [1] deleted {pkgbase} [2].\n\n'
'You will no longer receive notifications about this ' 'You will no longer receive notifications about this '
'package.', lang).format(user=self._user, 'package.', lang).format(user=self._user,
...@@ -432,7 +441,8 @@ class RequestOpenNotification(Notification): ...@@ -432,7 +441,8 @@ class RequestOpenNotification(Notification):
def __init__(self, conn, uid, reqid, reqtype, pkgbase_id, merge_into=None): def __init__(self, conn, uid, reqid, reqtype, pkgbase_id, merge_into=None):
self._user = username_from_id(conn, uid) self._user = username_from_id(conn, uid)
self._pkgbase = pkgbase_from_id(conn, pkgbase_id) self._pkgbase = pkgbase_from_id(conn, pkgbase_id)
cur = conn.execute('SELECT DISTINCT Users.Email FROM PackageRequests ' + cur = conn.execute(
'SELECT DISTINCT Users.Email FROM PackageRequests ' +
'INNER JOIN PackageBases ' + 'INNER JOIN PackageBases ' +
'ON PackageBases.ID = PackageRequests.PackageBaseID ' + 'ON PackageBases.ID = PackageRequests.PackageBaseID ' +
'INNER JOIN Users ' + 'INNER JOIN Users ' +
...@@ -489,7 +499,8 @@ class RequestOpenNotification(Notification): ...@@ -489,7 +499,8 @@ class RequestOpenNotification(Notification):
class RequestCloseNotification(Notification): class RequestCloseNotification(Notification):
def __init__(self, conn, uid, reqid, reason): def __init__(self, conn, uid, reqid, reason):
self._user = username_from_id(conn, uid) if int(uid) else None self._user = username_from_id(conn, uid) if int(uid) else None
cur = conn.execute('SELECT DISTINCT Users.Email FROM PackageRequests ' + cur = conn.execute(
'SELECT DISTINCT Users.Email FROM PackageRequests ' +
'INNER JOIN PackageBases ' + 'INNER JOIN PackageBases ' +
'ON PackageBases.ID = PackageRequests.PackageBaseID ' + 'ON PackageBases.ID = PackageRequests.PackageBaseID ' +
'INNER JOIN Users ' + 'INNER JOIN Users ' +
...@@ -563,11 +574,12 @@ class TUVoteReminderNotification(Notification): ...@@ -563,11 +574,12 @@ class TUVoteReminderNotification(Notification):
return self._recipients return self._recipients
def get_subject(self, lang): def get_subject(self, lang):
return self._l10n.translate('TU Vote Reminder: Proposal {id}', return aurweb.l10n.translator.translate(
'TU Vote Reminder: Proposal {id}',
lang).format(id=self._vote_id) lang).format(id=self._vote_id)
def get_body(self, lang): def get_body(self, lang):
return self._l10n.translate( return aurweb.l10n.translator.translate(
'Please remember to cast your vote on proposal {id} [1]. ' 'Please remember to cast your vote on proposal {id} [1]. '
'The voting period ends in less than 48 hours.', 'The voting period ends in less than 48 hours.',
lang).format(id=self._vote_id) lang).format(id=self._vote_id)
......
...@@ -19,6 +19,7 @@ name = YOUR_AUR_ROOT/aurweb.sqlite3 ...@@ -19,6 +19,7 @@ name = YOUR_AUR_ROOT/aurweb.sqlite3
aur_location = http://127.0.0.1:8080 aur_location = http://127.0.0.1:8080
disable_http_login = 0 disable_http_login = 0
enable-maintenance = 0 enable-maintenance = 0
localedir = YOUR_AUR_ROOT/web/locale
; Single sign-on; see doc/sso.txt. ; Single sign-on; see doc/sso.txt.
[sso] [sso]
......
""" Test our l10n module. """
from aurweb import l10n
class FakeRequest:
""" A fake Request doppleganger; use this to change request.cookies
easily and with no side-effects. """
def __init__(self, *args, **kwargs):
self.cookies = kwargs.pop("cookies", dict())
def test_translator():
""" Test creating l10n translation tools. """
de_home = l10n.translator.translate("Home", "de")
assert de_home == "Startseite"
def test_get_request_language():
""" First, tests default_lang, then tests a modified AURLANG cookie. """
request = FakeRequest()
assert l10n.get_request_language(request) == "en"
request.cookies["AURLANG"] = "de"
assert l10n.get_request_language(request) == "de"
def test_get_raw_translator_for_request():
""" Make sure that get_raw_translator_for_request is giving us
the translator we expect. """
request = FakeRequest(cookies={"AURLANG": "de"})
translator = l10n.get_raw_translator_for_request(request)
assert translator.gettext("Home") == \
l10n.translator.translate("Home", "de")
def test_get_translator_for_request():
""" Make sure that get_translator_for_request is giving us back
our expected translation function. """
request = FakeRequest(cookies={"AURLANG": "de"})
translate = l10n.get_translator_for_request(request)
assert translate("Home") == "Startseite"
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment