summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--aurweb/l10n.py79
-rwxr-xr-xaurweb/scripts/notify.py238
-rw-r--r--conf/config.dev1
-rw-r--r--test/__init__.py0
-rw-r--r--test/test_l10n.py44
6 files changed, 240 insertions, 123 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6db573d2..1e287748 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,6 +20,7 @@ test:
script:
- python setup.py install
- 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
- make -C test
- coverage report --include='aurweb/*'
diff --git a/aurweb/l10n.py b/aurweb/l10n.py
index a476ecd8..030ab274 100644
--- a/aurweb/l10n.py
+++ b/aurweb/l10n.py
@@ -1,24 +1,79 @@
import gettext
+import typing
+
+from collections import OrderedDict
+
+from fastapi import Request
+from jinja2 import contextfilter
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:
def __init__(self):
self._localedir = aurweb.config.get('options', 'localedir')
self._translator = {}
- def translate(self, s, lang):
- if lang == 'en':
- return s
+ def get_translator(self, lang: str):
if lang not in self._translator:
self._translator[lang] = gettext.translation("aurweb",
self._localedir,
- languages=[lang])
- return self._translator[lang].gettext(s)
+ languages=[lang],
+ 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
translator function for it.
@@ -29,12 +84,16 @@ def get_translator_for_request(request):
print(_("Hello"))
```
"""
- lang = request.cookies.get("AURLANG")
- if lang is None:
- lang = aurweb.config.get("options", "default_lang")
- translator = Translator()
+ lang = get_request_language(request)
def translate(message):
return translator.translate(message, lang)
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)
diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py
index 7f8e7168..1df0175a 100755
--- a/aurweb/scripts/notify.py
+++ b/aurweb/scripts/notify.py
@@ -40,9 +40,6 @@ def pkgbase_from_pkgreq(conn, reqid):
class Notification:
- def __init__(self):
- self._l10n = aurweb.l10n.Translator()
-
def get_refs(self):
return ()
@@ -97,9 +94,12 @@ class Notification:
else:
# send email using smtplib; no local MTA required
server_addr = aurweb.config.get('notifications', 'smtp-server')
- server_port = aurweb.config.getint('notifications', 'smtp-port')
- use_ssl = aurweb.config.getboolean('notifications', 'smtp-use-ssl')
- use_starttls = aurweb.config.getboolean('notifications', 'smtp-use-starttls')
+ server_port = aurweb.config.getint('notifications',
+ 'smtp-port')
+ 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')
passwd = aurweb.config.get('notifications', 'smtp-password')
@@ -127,7 +127,8 @@ class ResetKeyNotification(Notification):
cur = conn.execute('SELECT UserName, Email, BackupEmail, ' +
'LangPreference, ResetKey ' +
'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__()
def get_recipients(self):
@@ -137,15 +138,15 @@ class ResetKeyNotification(Notification):
return [(self._to, 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):
- return self._l10n.translate(
- 'A password reset request was submitted for the account '
- '{user} associated with your email address. If you wish to '
- 'reset your password follow the link [1] below, otherwise '
- 'ignore this message and nothing will happen.',
- lang).format(user=self._username)
+ return aurweb.l10n.translator.translate(
+ 'A password reset request was submitted for the account '
+ '{user} associated with your email address. If you wish to '
+ 'reset your password follow the link [1] below, otherwise '
+ 'ignore this message and nothing will happen.',
+ lang).format(user=self._username)
def get_refs(self):
return (aur_location + '/passreset/?resetkey=' + self._resetkey,)
@@ -153,15 +154,16 @@ class ResetKeyNotification(Notification):
class WelcomeNotification(ResetKeyNotification):
def get_subject(self, lang):
- return self._l10n.translate('Welcome to the Arch User Repository',
- lang)
+ return aurweb.l10n.translator.translate(
+ 'Welcome to the Arch User Repository',
+ lang)
def get_body(self, lang):
- return self._l10n.translate(
- 'Welcome to the Arch User Repository! In order to set an '
- 'initial password for your new account, please click the '
- 'link [1] below. If the link does not work, try copying and '
- 'pasting it into your browser.', lang)
+ return aurweb.l10n.translator.translate(
+ 'Welcome to the Arch User Repository! In order to set an '
+ 'initial password for your new account, please click the '
+ 'link [1] below. If the link does not work, try copying and '
+ 'pasting it into your browser.', lang)
class CommentNotification(Notification):
@@ -186,19 +188,21 @@ class CommentNotification(Notification):
return self._recipients
def get_subject(self, lang):
- return self._l10n.translate('AUR Comment for {pkgbase}',
- lang).format(pkgbase=self._pkgbase)
+ return aurweb.l10n.translator.translate(
+ 'AUR Comment for {pkgbase}',
+ lang).format(pkgbase=self._pkgbase)
def get_body(self, lang):
- body = self._l10n.translate(
- '{user} [1] added the following comment to {pkgbase} [2]:',
- lang).format(user=self._user, pkgbase=self._pkgbase)
+ body = aurweb.l10n.translator.translate(
+ '{user} [1] added the following comment to {pkgbase} [2]:',
+ lang).format(user=self._user, pkgbase=self._pkgbase)
body += '\n\n' + self._text + '\n\n-- \n'
- dnlabel = self._l10n.translate('Disable notifications', lang)
- body += self._l10n.translate(
- 'If you no longer wish to receive notifications about this '
- 'package, please go to the package page [2] and select '
- '"{label}".', lang).format(label=dnlabel)
+ dnlabel = aurweb.l10n.translator.translate(
+ 'Disable notifications', lang)
+ body += aurweb.l10n.translator.translate(
+ 'If you no longer wish to receive notifications about this '
+ 'package, please go to the package page [2] and select '
+ '"{label}".', lang).format(label=dnlabel)
return body
def get_refs(self):
@@ -231,20 +235,21 @@ class UpdateNotification(Notification):
return self._recipients
def get_subject(self, lang):
- return self._l10n.translate('AUR Package Update: {pkgbase}',
- lang).format(pkgbase=self._pkgbase)
+ return aurweb.l10n.translator.translate(
+ 'AUR Package Update: {pkgbase}',
+ lang).format(pkgbase=self._pkgbase)
def get_body(self, lang):
- body = self._l10n.translate('{user} [1] pushed a new commit to '
- '{pkgbase} [2].', lang).format(
- user=self._user,
- pkgbase=self._pkgbase)
+ body = aurweb.l10n.translator.translate(
+ '{user} [1] pushed a new commit to {pkgbase} [2].',
+ lang).format(user=self._user, pkgbase=self._pkgbase)
body += '\n\n-- \n'
- dnlabel = self._l10n.translate('Disable notifications', lang)
- body += self._l10n.translate(
- 'If you no longer wish to receive notifications about this '
- 'package, please go to the package page [2] and select '
- '"{label}".', lang).format(label=dnlabel)
+ dnlabel = aurweb.l10n.translator.translate(
+ 'Disable notifications', lang)
+ body += aurweb.l10n.translator.translate(
+ 'If you no longer wish to receive notifications about this '
+ 'package, please go to the package page [2] and select '
+ '"{label}".', lang).format(label=dnlabel)
return body
def get_refs(self):
@@ -261,15 +266,16 @@ class FlagNotification(Notification):
def __init__(self, conn, uid, pkgbase_id):
self._user = username_from_id(conn, uid)
self._pkgbase = pkgbase_from_id(conn, pkgbase_id)
- cur = conn.execute('SELECT DISTINCT Users.Email, ' +
- 'Users.LangPreference FROM Users ' +
- 'LEFT JOIN PackageComaintainers ' +
- 'ON PackageComaintainers.UsersID = Users.ID ' +
- 'INNER JOIN PackageBases ' +
- 'ON PackageBases.MaintainerUID = Users.ID OR ' +
- 'PackageBases.ID = PackageComaintainers.PackageBaseID ' +
- 'WHERE PackageBases.ID = ? AND ' +
- 'Users.Suspended = 0', [pkgbase_id])
+ cur = conn.execute(
+ 'SELECT DISTINCT Users.Email, ' +
+ 'Users.LangPreference FROM Users ' +
+ 'LEFT JOIN PackageComaintainers ' +
+ 'ON PackageComaintainers.UsersID = Users.ID ' +
+ 'INNER JOIN PackageBases ' +
+ 'ON PackageBases.MaintainerUID = Users.ID OR ' +
+ 'PackageBases.ID = PackageComaintainers.PackageBaseID ' +
+ 'WHERE PackageBases.ID = ? AND ' +
+ 'Users.Suspended = 0', [pkgbase_id])
self._recipients = cur.fetchall()
cur = conn.execute('SELECT FlaggerComment FROM PackageBases WHERE ' +
'ID = ?', [pkgbase_id])
@@ -280,15 +286,15 @@ class FlagNotification(Notification):
return self._recipients
def get_subject(self, lang):
- return self._l10n.translate('AUR Out-of-date Notification for '
- '{pkgbase}',
- lang).format(pkgbase=self._pkgbase)
+ return aurweb.l10n.translator.translate(
+ 'AUR Out-of-date Notification for {pkgbase}',
+ lang).format(pkgbase=self._pkgbase)
def get_body(self, lang):
- body = self._l10n.translate(
- 'Your package {pkgbase} [1] has been flagged out-of-date by '
- '{user} [2]:', lang).format(pkgbase=self._pkgbase,
- user=self._user)
+ body = aurweb.l10n.translator.translate(
+ 'Your package {pkgbase} [1] has been flagged out-of-date by '
+ '{user} [2]:', lang).format(pkgbase=self._pkgbase,
+ user=self._user)
body += '\n\n' + self._text
return body
@@ -320,8 +326,9 @@ class OwnershipEventNotification(Notification):
return self._recipients
def get_subject(self, lang):
- return self._l10n.translate('AUR Ownership Notification for {pkgbase}',
- lang).format(pkgbase=self._pkgbase)
+ return aurweb.l10n.translator.translate(
+ 'AUR Ownership Notification for {pkgbase}',
+ lang).format(pkgbase=self._pkgbase)
def get_refs(self):
return (aur_location + '/pkgbase/' + self._pkgbase + '/',
@@ -330,17 +337,17 @@ class OwnershipEventNotification(Notification):
class AdoptNotification(OwnershipEventNotification):
def get_body(self, lang):
- return self._l10n.translate(
- 'The package {pkgbase} [1] was adopted by {user} [2].',
- lang).format(pkgbase=self._pkgbase, user=self._user)
+ return aurweb.l10n.translator.translate(
+ 'The package {pkgbase} [1] was adopted by {user} [2].',
+ lang).format(pkgbase=self._pkgbase, user=self._user)
class DisownNotification(OwnershipEventNotification):
def get_body(self, lang):
- return self._l10n.translate(
- 'The package {pkgbase} [1] was disowned by {user} '
- '[2].', lang).format(pkgbase=self._pkgbase,
- user=self._user)
+ return aurweb.l10n.translator.translate(
+ 'The package {pkgbase} [1] was disowned by {user} '
+ '[2].', lang).format(pkgbase=self._pkgbase,
+ user=self._user)
class ComaintainershipEventNotification(Notification):
@@ -355,9 +362,9 @@ class ComaintainershipEventNotification(Notification):
return [(self._to, self._lang)]
def get_subject(self, lang):
- return self._l10n.translate('AUR Co-Maintainer Notification for '
- '{pkgbase}',
- lang).format(pkgbase=self._pkgbase)
+ return aurweb.l10n.translator.translate(
+ 'AUR Co-Maintainer Notification for {pkgbase}',
+ lang).format(pkgbase=self._pkgbase)
def get_refs(self):
return (aur_location + '/pkgbase/' + self._pkgbase + '/',)
@@ -365,16 +372,16 @@ class ComaintainershipEventNotification(Notification):
class ComaintainerAddNotification(ComaintainershipEventNotification):
def get_body(self, lang):
- return self._l10n.translate(
- 'You were added to the co-maintainer list of {pkgbase} [1].',
- lang).format(pkgbase=self._pkgbase)
+ return aurweb.l10n.translator.translate(
+ 'You were added to the co-maintainer list of {pkgbase} [1].',
+ lang).format(pkgbase=self._pkgbase)
class ComaintainerRemoveNotification(ComaintainershipEventNotification):
def get_body(self, lang):
- return self._l10n.translate(
- 'You were removed from the co-maintainer list of {pkgbase} '
- '[1].', lang).format(pkgbase=self._pkgbase)
+ return aurweb.l10n.translator.translate(
+ 'You were removed from the co-maintainer list of {pkgbase} '
+ '[1].', lang).format(pkgbase=self._pkgbase)
class DeleteNotification(Notification):
@@ -400,25 +407,27 @@ class DeleteNotification(Notification):
return self._recipients
def get_subject(self, lang):
- return self._l10n.translate('AUR Package deleted: {pkgbase}',
- lang).format(pkgbase=self._old_pkgbase)
+ return aurweb.l10n.translator.translate(
+ 'AUR Package deleted: {pkgbase}',
+ lang).format(pkgbase=self._old_pkgbase)
def get_body(self, lang):
if self._new_pkgbase:
- dnlabel = self._l10n.translate('Disable notifications', lang)
- return self._l10n.translate(
- '{user} [1] merged {old} [2] into {new} [3].\n\n'
- '-- \n'
- 'If you no longer wish receive notifications about the '
- 'new package, please go to [3] and click "{label}".',
- lang).format(user=self._user, old=self._old_pkgbase,
- new=self._new_pkgbase, label=dnlabel)
+ dnlabel = aurweb.l10n.translator.translate(
+ 'Disable notifications', lang)
+ return aurweb.l10n.translator.translate(
+ '{user} [1] merged {old} [2] into {new} [3].\n\n'
+ '-- \n'
+ 'If you no longer wish receive notifications about the '
+ 'new package, please go to [3] and click "{label}".',
+ lang).format(user=self._user, old=self._old_pkgbase,
+ new=self._new_pkgbase, label=dnlabel)
else:
- return self._l10n.translate(
- '{user} [1] deleted {pkgbase} [2].\n\n'
- 'You will no longer receive notifications about this '
- 'package.', lang).format(user=self._user,
- pkgbase=self._old_pkgbase)
+ return aurweb.l10n.translator.translate(
+ '{user} [1] deleted {pkgbase} [2].\n\n'
+ 'You will no longer receive notifications about this '
+ 'package.', lang).format(user=self._user,
+ pkgbase=self._old_pkgbase)
def get_refs(self):
refs = (aur_location + '/account/' + self._user + '/',
@@ -432,14 +441,15 @@ class RequestOpenNotification(Notification):
def __init__(self, conn, uid, reqid, reqtype, pkgbase_id, merge_into=None):
self._user = username_from_id(conn, uid)
self._pkgbase = pkgbase_from_id(conn, pkgbase_id)
- cur = conn.execute('SELECT DISTINCT Users.Email FROM PackageRequests ' +
- 'INNER JOIN PackageBases ' +
- 'ON PackageBases.ID = PackageRequests.PackageBaseID ' +
- 'INNER JOIN Users ' +
- 'ON Users.ID = PackageRequests.UsersID ' +
- 'OR Users.ID = PackageBases.MaintainerUID ' +
- 'WHERE PackageRequests.ID = ? AND ' +
- 'Users.Suspended = 0', [reqid])
+ cur = conn.execute(
+ 'SELECT DISTINCT Users.Email FROM PackageRequests ' +
+ 'INNER JOIN PackageBases ' +
+ 'ON PackageBases.ID = PackageRequests.PackageBaseID ' +
+ 'INNER JOIN Users ' +
+ 'ON Users.ID = PackageRequests.UsersID ' +
+ 'OR Users.ID = PackageBases.MaintainerUID ' +
+ 'WHERE PackageRequests.ID = ? AND ' +
+ 'Users.Suspended = 0', [reqid])
self._to = aurweb.config.get('options', 'aur_request_ml')
self._cc = [row[0] for row in cur.fetchall()]
cur = conn.execute('SELECT Comments FROM PackageRequests WHERE ID = ?',
@@ -489,14 +499,15 @@ class RequestOpenNotification(Notification):
class RequestCloseNotification(Notification):
def __init__(self, conn, uid, reqid, reason):
self._user = username_from_id(conn, uid) if int(uid) else None
- cur = conn.execute('SELECT DISTINCT Users.Email FROM PackageRequests ' +
- 'INNER JOIN PackageBases ' +
- 'ON PackageBases.ID = PackageRequests.PackageBaseID ' +
- 'INNER JOIN Users ' +
- 'ON Users.ID = PackageRequests.UsersID ' +
- 'OR Users.ID = PackageBases.MaintainerUID ' +
- 'WHERE PackageRequests.ID = ? AND ' +
- 'Users.Suspended = 0', [reqid])
+ cur = conn.execute(
+ 'SELECT DISTINCT Users.Email FROM PackageRequests ' +
+ 'INNER JOIN PackageBases ' +
+ 'ON PackageBases.ID = PackageRequests.PackageBaseID ' +
+ 'INNER JOIN Users ' +
+ 'ON Users.ID = PackageRequests.UsersID ' +
+ 'OR Users.ID = PackageBases.MaintainerUID ' +
+ 'WHERE PackageRequests.ID = ? AND ' +
+ 'Users.Suspended = 0', [reqid])
self._to = aurweb.config.get('options', 'aur_request_ml')
self._cc = [row[0] for row in cur.fetchall()]
cur = conn.execute('SELECT PackageRequests.ClosureComment, ' +
@@ -563,14 +574,15 @@ class TUVoteReminderNotification(Notification):
return self._recipients
def get_subject(self, lang):
- return self._l10n.translate('TU Vote Reminder: Proposal {id}',
- lang).format(id=self._vote_id)
+ return aurweb.l10n.translator.translate(
+ 'TU Vote Reminder: Proposal {id}',
+ lang).format(id=self._vote_id)
def get_body(self, lang):
- return self._l10n.translate(
- 'Please remember to cast your vote on proposal {id} [1]. '
- 'The voting period ends in less than 48 hours.',
- lang).format(id=self._vote_id)
+ return aurweb.l10n.translator.translate(
+ 'Please remember to cast your vote on proposal {id} [1]. '
+ 'The voting period ends in less than 48 hours.',
+ lang).format(id=self._vote_id)
def get_refs(self):
return (aur_location + '/tu/?id=' + str(self._vote_id),)
diff --git a/conf/config.dev b/conf/config.dev
index 37f38c45..ef7b5ed7 100644
--- a/conf/config.dev
+++ b/conf/config.dev
@@ -19,6 +19,7 @@ name = YOUR_AUR_ROOT/aurweb.sqlite3
aur_location = http://127.0.0.1:8080
disable_http_login = 0
enable-maintenance = 0
+localedir = YOUR_AUR_ROOT/web/locale
; Single sign-on; see doc/sso.txt.
[sso]
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/__init__.py
diff --git a/test/test_l10n.py b/test/test_l10n.py
new file mode 100644
index 00000000..1a1ef3e6
--- /dev/null
+++ b/test/test_l10n.py
@@ -0,0 +1,44 @@
+""" 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"