Skip to content

Commit 1e9f746

Browse files
authored
Feature/i18n (#174)
* feat: add i18n support This feature includes the core functions added to /internal, English and Hebrew json files, removal of text strings from the html files, updates to the routes and model updates. Also included are tests. Please note, that this feature is waiting on user registration for final hooks. * fix(i18n): correct flake8 line length errors * fix(i18n): attempt to fix build crash at get_translations_dicts() and move foreignkey to last column as requested in CR. Additional minor doc fixes. * fix(i18n): simplify check of language file path between live and testing This change hopefully adds this line to the code coverage. * fix(i18n): replace glob with path.glob This change reduces the amount of imports needed and also uses one dir path for both tests and live. * fix(i18n): revert attempted code improvement * fix(i18n): added missing language_id to placeholder user * fix(i18n): added simple test for home * fix(i18n): code review fixes * fix(i18n): split the html result dict into two sub-dicts, "variables" and "localized" and the creation of the Jinja2TemplateResponse is now handled in internal/utilities. * fix(i18n): fixed CR request to not use global state for translations The translations are now handled with @lru_cache and the function is only re-handled when a new language code parameter is used. This should be the last edit (unless any crash) to this version of the translation feature.
1 parent 27a6cf7 commit 1e9f746

26 files changed

+658
-73
lines changed

app/babel_mapping.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Extraction from Python source files
2+
3+
[python: **.py]
4+
5+
# Extraction from Jinja2 HTML and text templates
6+
7+
[jinja2: **/templates/**.html]
8+
extensions=jinja2.ext.i18n,jinja2.ext.autoescape,jinja2.ext.with_

app/config.py.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ MEDIA_DIRECTORY = 'media'
2727
PICTURE_EXTENSION = '.png'
2828
AVATAR_SIZE = (120, 120)
2929

30+
31+
# DEFAULT WEBSITE LANGUAGE
32+
WEBSITE_LANGUAGE = "en"
33+
3034
# API-KEYS
3135
# Get a free API KEY for Astronomy feature @ www.weatherapi.com/signup.aspx
3236
ASTRONOMY_API_KEY = os.getenv('ASTRONOMY_API_KEY')

app/database/models.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class User(Base):
4242
avatar = Column(String, default="profile.png")
4343
telegram_id = Column(String, unique=True)
4444
is_active = Column(Boolean, default=False)
45-
45+
language_id = Column(Integer, ForeignKey("languages.id"))
4646
events = relationship("UserEvent", back_populates="participants")
4747

4848
def __repr__(self):
@@ -79,6 +79,13 @@ def __repr__(self):
7979
return f'<Event {self.id}>'
8080

8181

82+
class Language(Base):
83+
__tablename__ = "languages"
84+
85+
id = Column(Integer, primary_key=True, index=True)
86+
name = Column(String, unique=True, nullable=False)
87+
88+
8289
class Category(Base):
8390
__tablename__ = "categories"
8491

app/dependencies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
from app import config
77
from app.internal.logger_customizer import LoggerCustomizer
88

9-
109
APP_PATH = os.path.dirname(os.path.realpath(__file__))
1110
MEDIA_PATH = os.path.join(APP_PATH, config.MEDIA_DIRECTORY)
1211
STATIC_PATH = os.path.join(APP_PATH, "static")
1312
TEMPLATES_PATH = os.path.join(APP_PATH, "templates")
1413

1514
templates = Jinja2Templates(directory=TEMPLATES_PATH)
15+
templates.env.add_extension('jinja2.ext.i18n')
1616

1717
# Configure logger
1818
logger = LoggerCustomizer.make_logger(config.LOG_PATH,

app/internal/languages.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import gettext
2+
import os
3+
from pathlib import Path
4+
from typing import Any, Generator
5+
6+
from app import config
7+
from app.dependencies import templates
8+
9+
LANGUAGE_DIR = "app/locales"
10+
LANGUAGE_DIR_TEST = "../app/locales"
11+
TRANSLATION_FILE = "base"
12+
13+
14+
def set_ui_language(language: str = None) -> None:
15+
"""Set the gettext translations to a given language.
16+
If the language requested is not supported, the translations default
17+
to the value of config.WEBSITE_LANGUAGE.
18+
19+
Args:
20+
language (str, optional): a valid language code that follows RFC 1766.
21+
Defaults to None.
22+
See also the Language Code Identifier (LCID) Reference for a list of
23+
valid language codes.
24+
25+
.. _RFC 1766:
26+
https://tools.ietf.org/html/rfc1766.html
27+
28+
.. _Language Code Identifier (LCID) Reference:
29+
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c # noqa: E501
30+
"""
31+
32+
# TODO: Connect when user registration is completed.
33+
# if not language:
34+
# language = _get_display_language(user_id: int)
35+
36+
language_dir = _get_language_directory()
37+
38+
if language not in list(_get_supported_languages(language_dir)):
39+
language = config.WEBSITE_LANGUAGE
40+
41+
translations = gettext.translation(TRANSLATION_FILE,
42+
localedir=language_dir,
43+
languages=[language])
44+
translations.install()
45+
templates.env.install_gettext_translations(translations, newstyle=True)
46+
47+
48+
# TODO: Waiting for user registration. Add doc.
49+
# def _get_display_language(user_id: int) -> str:
50+
# # TODO: handle user language setting:
51+
# # If user is logged in, get language setting.
52+
# # If user is not logged in, get default site setting.
53+
#
54+
# if db_user:
55+
# return db_user.language
56+
# return config.WEBSITE_LANGUAGE
57+
58+
59+
def _get_language_directory() -> str:
60+
"""Get and return the language directory relative path.
61+
62+
Returns:
63+
str: the language directory relative path.
64+
"""
65+
language_dir = LANGUAGE_DIR
66+
if Path(LANGUAGE_DIR_TEST).is_dir():
67+
# If running from test, change dir path.
68+
language_dir = LANGUAGE_DIR_TEST
69+
return language_dir
70+
71+
72+
def _get_supported_languages(language_dir: str = _get_language_directory()) \
73+
-> Generator[str, Any, None]:
74+
"""Get and return a generator of supported translation languages codes.
75+
76+
Args:
77+
language_dir (str, optional): the path of the language directory.
78+
Defaults to the return value of _get_language_directory().
79+
80+
Returns:
81+
Generator[str, Any, None]: a generator expression of supported
82+
translation languages codes.
83+
"""
84+
85+
paths = [Path(f.path) for f in os.scandir(language_dir) if f.is_dir()]
86+
return (language.name for language in paths)

app/locales/base.pot

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Translations template for PROJECT.
2+
# Copyright (C) 2021 ORGANIZATION
3+
# This file is distributed under the same license as the PROJECT project.
4+
# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
5+
#
6+
#, fuzzy
7+
msgid ""
8+
msgstr ""
9+
"Project-Id-Version: PROJECT VERSION\n"
10+
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
11+
"POT-Creation-Date: 2021-02-01 10:21+0200\n"
12+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14+
"Language-Team: LANGUAGE <LL@li.org>\n"
15+
"MIME-Version: 1.0\n"
16+
"Content-Type: text/plain; charset=utf-8\n"
17+
"Content-Transfer-Encoding: 8bit\n"
18+
"Generated-By: Babel 2.9.0\n"
19+
20+
#: app/routers/profile.py:19
21+
msgid "Not found"
22+
msgstr ""
23+
24+
#: app/templates/agenda.html:11
25+
msgid "From"
26+
msgstr ""
27+
28+
#: app/templates/agenda.html:13
29+
msgid "to"
30+
msgstr ""
31+
32+
#: app/templates/agenda.html:15
33+
msgid "Get Agenda"
34+
msgstr ""
35+
36+
#: app/templates/agenda.html:20
37+
msgid "Today"
38+
msgstr ""
39+
40+
#: app/templates/agenda.html:23
41+
msgid "Next week"
42+
msgstr ""
43+
44+
#: app/templates/agenda.html:26
45+
msgid "Next month"
46+
msgstr ""
47+
48+
#: app/templates/agenda.html:33
49+
msgid "Start date is greater than end date"
50+
msgstr ""
51+
52+
#: app/templates/agenda.html:35
53+
msgid "No events found..."
54+
msgstr ""
55+
56+
#: app/templates/base.html:18
57+
msgid "Calendar"
58+
msgstr ""
59+
60+
#: app/templates/base.html:26
61+
msgid "Home"
62+
msgstr ""
63+
64+
#: app/templates/base.html:29
65+
msgid "Profile"
66+
msgstr ""
67+
68+
#: app/templates/base.html:32
69+
msgid "Sign in"
70+
msgstr ""
71+
72+
#: app/templates/base.html:35
73+
msgid "Sign up"
74+
msgstr ""
75+
76+
#: app/templates/base.html:40
77+
msgid "Agenda"
78+
msgstr ""
79+
80+
#: app/templates/profile.html:59
81+
msgid "Update name"
82+
msgstr ""
83+
84+
#: app/templates/profile.html:66 app/templates/profile.html:88
85+
#: app/templates/profile.html:109 app/templates/profile.html:133
86+
msgid "Save changes"
87+
msgstr ""
88+
89+
#: app/templates/profile.html:80
90+
msgid "Update email"
91+
msgstr ""
92+
93+
#: app/templates/profile.html:101
94+
msgid "Update description"
95+
msgstr ""
96+
97+
#: app/templates/profile.html:124
98+
msgid "Update photo"
99+
msgstr ""
100+
101+
#: app/templates/profile.html:176
102+
msgid "Settings"
103+
msgstr ""
104+
105+
#: app/templates/profile.html:207
106+
msgid "Your feature"
107+
msgstr ""
108+
109+
#: app/templates/profile.html:228
110+
msgid "Upcoming event on (date)"
111+
msgstr ""
112+
113+
#: app/templates/profile.html:239
114+
msgid "The Event (event)"
115+
msgstr ""
116+
117+
#: app/templates/profile.html:242
118+
msgid "Last updated (time) ago"
119+
msgstr ""
120+
121+
#: app/templates/profile.html:259
122+
msgid "Explore MeetUps near you"
123+
msgstr ""
124+
125+
#: app/templates/profile.html:268 app/templates/profile.html:277
126+
msgid "Your Card"
127+
msgstr ""
128+
129+
#. i18n: String used in testing. Do not change.
130+
#: tests/test_language.py:32
131+
msgid "test python translation"
132+
msgstr ""
133+

app/locales/en/LC_MESSAGES/base.mo

443 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)