Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0f8b20c
feat: add i18n support
sMOKIK Jan 22, 2021
3cecc97
fix(i18n): correct flake8 line length errors
sMOKIK Jan 22, 2021
65db418
fix(i18n): attempt to fix build crash at get_translations_dicts() and…
sMOKIK Jan 22, 2021
feba1bb
fix(i18n): simplify check of language file path between live and testing
sMOKIK Jan 22, 2021
72f3a2b
fix(i18n): replace glob with path.glob
sMOKIK Jan 23, 2021
8d85279
fix(i18n): revert attempted code improvement
sMOKIK Jan 23, 2021
d75fd94
fix(i18n): added missing language_id to placeholder user
sMOKIK Jan 23, 2021
86925ad
fix(i18n): added simple test for home
sMOKIK Jan 23, 2021
eaf1107
fix(i18n): code review fixes
sMOKIK Jan 24, 2021
b69042d
fix(i18n): split the html result dict into two sub-dicts, "variables"…
sMOKIK Jan 25, 2021
abc7641
fix(i18n): fixed CR request to not use global state for translations
sMOKIK Jan 25, 2021
6f5728d
fix(i18n): add test to complete code coverage
sMOKIK Jan 25, 2021
e1caaa4
fix(i18n): add missing documentation
sMOKIK Jan 25, 2021
ade7c25
fix(i18n): i18n v2.0
sMOKIK Jan 26, 2021
ae49fce
fix(i18n): temporarily disable F821 errors on built-in _() function
sMOKIK Jan 26, 2021
c6a0c3b
fix(i18n): Change function name
sMOKIK Jan 26, 2021
4bf46ee
fix(i18n): change function name
sMOKIK Jan 26, 2021
c00d714
fix(i18n): add test with no language code and with invalid arguments
sMOKIK Jan 26, 2021
92b585b
fix(i18n): html and documentation fixes
sMOKIK Jan 27, 2021
f4861a8
fix(i18n): add missing documentation fixes from previous commit
sMOKIK Jan 27, 2021
80da4a1
fix(i18n): move 'jinja2.ext.i18n' extension setting to dependencies.py
sMOKIK Jan 27, 2021
0e7d2e7
fix(i18n): code review fixes
sMOKIK Jan 27, 2021
b0a1358
fix(i18n): lint error line length
sMOKIK Jan 27, 2021
5a4e5d3
fix(i18n): hopefully fix code coverage
sMOKIK Jan 27, 2021
46d078e
fix(i18n): small doc fix
sMOKIK Jan 27, 2021
b1d7d30
fix(i18n): testing if this fix works
sMOKIK Jan 27, 2021
a34d3ce
fix(i18n): another fix test
sMOKIK Jan 27, 2021
f96f221
fix(i18n): remove test, didn't work
sMOKIK Jan 27, 2021
5784a09
fix(i18n): try another method of monkeypatch
sMOKIK Jan 27, 2021
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
3 changes: 3 additions & 0 deletions app/config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ MEDIA_DIRECTORY = 'media'
PICTURE_EXTENSION = '.png'
AVATAR_SIZE = (120, 120)

# DEFAULT WEBSITE LANGUAGE
WEBSITE_LANGUAGE = "en"

email_conf = ConnectionConfig(
MAIL_USERNAME=os.getenv("MAIL_USERNAME") or "user",
MAIL_PASSWORD=os.getenv("MAIL_PASSWORD") or "password",
Expand Down
9 changes: 8 additions & 1 deletion app/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ class User(Base):
full_name = Column(String)
description = Column(String, default="Happy new user!")
avatar = Column(String, default="profile.png")

is_active = Column(Boolean, default=True)
language_id = Column(Integer, ForeignKey("languages.id"))

events = relationship(
"Event", cascade="all, delete", back_populates="owner")
Expand All @@ -32,3 +32,10 @@ class Event(Base):
owner_id = Column(Integer, ForeignKey("users.id"))

owner = relationship("User", back_populates="events")


class Language(Base):
__tablename__ = "languages"

id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, nullable=False)
100 changes: 100 additions & 0 deletions app/internal/languages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from functools import lru_cache
import glob
import json
from pathlib import PureWindowsPath
from typing import Dict, Union

from app import config

LANGUAGE_FILES_PATH = "app/languages/*.json"
LANGUAGE_FILES_PATH_TEST = "../app/languages/*.json"


@lru_cache()
def get_translation_words(display_language: str = None) -> \
Dict[str, Union[str, Dict[str, str]]]:
"""Gets and returns the translation words for a given language.
The returned object is a dictionary of the translated words in either
the user's language setting, or the default app setting.

Using the @lru_cache() decorator makes the function return the same
translation for a given language that was previously used, instead of
computing it again, executing the code of the function every time.

Args:
display_language (str): a valid code that follows RFC 1766.
See also the Language Code Identifier (LCID) Reference for a list of
valid codes.

Returns:
Dict[str, Union[str, Dict[str, str]]]: a dictionary of string keys and
their translation as their values. The value can either be a string,
or a nested dictionary for plural translations.

.. _RFC 1766:
https://tools.ietf.org/html/rfc1766.html

.. _Language Code Identifier (LCID) Reference:
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c?redirectedfrom=MSDN # noqa: E501
"""

if display_language:
return _populate_with_language(display_language)
else:
# TODO: Waiting for user registration. Restore when done.
# display_language = _get_display_language(user_id)
# return populate_with_language(display_language)
return _populate_with_language(config.WEBSITE_LANGUAGE)


# TODO: Waiting for user registration. Add doc.
# def _get_display_language(user_id: int) -> str:
# # TODO: handle user language setting:
# # If user is logged in, get language setting.
# # If user is not logged in, get default site setting.
#
# if db_user:
# return db_user.language
# return config.WEBSITE_LANGUAGE


def _populate_with_language(display_language: str) -> \
Dict[str, Union[str, Dict[str, str]]]:
"""Updates the translation_words to the requested language.
If the language code is not supported by the applications, the dictionary
defaults to the config.WEBSITE_LANGUAGE setting.

Args:
display_language (str): a valid code that follows RFC 1766.
See also the Language Code Identifier (LCID) Reference for a list of
valid codes.

Returns:
Dict[str, Union[str, Dict[str, str]]]: a dictionary of string keys and
their translation as their values. The value can either be a string,
or a nested dictionary for plural translations.
"""
translation_words_all_languages = _get_translation_words_all_languages()
if display_language in translation_words_all_languages:
return translation_words_all_languages[display_language]
return translation_words_all_languages[config.WEBSITE_LANGUAGE]


def _get_translation_words_all_languages() -> \
Dict[str, Dict[str, Union[str, Dict[str, str]]]]:
"""Gets and returns a dictionary of nested language dictionaries from
the language translation files.

Returns:
Dict[str, Dict[str, Union[str, Dict[str, str]]]]: a dictionary of
language codes as string keys, and nested dictionaries of translations
as their values.
"""
language_translations = {}
language_files = (glob.glob(LANGUAGE_FILES_PATH)
or glob.glob(LANGUAGE_FILES_PATH_TEST))
for language_file in language_files:
language_code = PureWindowsPath(language_file).stem
with open(language_file, 'r', encoding='utf8') as file:
language_translations[language_code] = json.load(file)
return language_translations
30 changes: 30 additions & 0 deletions app/internal/utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import Any, Dict

from fastapi import Request
from fastapi.templating import Jinja2Templates

from app.dependencies import templates
from app.internal import languages


def get_template_response(html_file_name: str, request: Request,
variables: Dict[str, Any] = None) -> Jinja2Templates:
"""Creates and returns a Jinja2Templates object with a result dictionary
containing three parts: the request object, a variables dictionary,
and a translation dictionary.

Args:
html_file_name (str): the name of the html file.
request (Request): a FastApi Request object.
variables (Dict[str, Any]): an optional variables dictionary used
in the html file.

Returns:
Jinja2Templates: a Jinja2Templates response object.
"""
translations = languages.get_translation_words()
result = {"request": request,
"variables": variables,
"localized": translations,
}
return templates.TemplateResponse(html_file_name, result)
31 changes: 31 additions & 0 deletions app/languages/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"website_title": "Calendar",
"calendar_button": "Calendar",
"home_button": "Home",
"profile_button": "Profile",
"sign_in_button": "Sign In",
"sign_up_button": "Sign Up",
"agenda_button": "Agenda",
"edit_full_name_button": "Edit full name",
"edit_email_button": "Edit email",
"edit_photo_button": "Edit photo",
"edit_description_button": "Edit description",
"update_full_name_title": "Update full name",
"update_email_title": "Update email",
"update_photo_title": "Upload photo",
"update_description_title": "Update description",
"save_changes_button": "Save changes",
"settings_button": "Settings",
"features_title": "Features",
"export_calendar_button": "Export my calendar",
"upcoming_event_on_date": "Upcoming event on {date}",
"from_button": "From",
"to_button": "to",
"get_agenda_button": "Get Agenda",
"agenda_today": "Today",
"agenda_next_week": "Next week",
"agenda_next_month": "Next month",
"agenda_invalid_date": "Start date is greater than end date",
"no_events_found": "No events found...",
"test_word": "test"
}
31 changes: 31 additions & 0 deletions app/languages/he.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"website_title": "Calendar",
"calendar_button": "Calendar",
"home_button": "Home",
"profile_button": "פרופיל",
"sign_in_button": "Sign In",
"sign_up_button": "Sign Up",
"agenda_button": "Agenda",
"edit_full_name_button": "ערוך שם מלא",
"edit_email_button": "ערוך אימייל",
"edit_photo_button": "ערוך תמונה",
"edit_description_button": "ערוך תיאור",
"update_full_name_title": "עדכן שם מלא",
"update_email_title": "עדכן אימייל",
"update_photo_title": "עדכן תמונה",
"update_description_title": "עדכן תיאור",
"save_changes_button": "שמור שינויים",
"settings_button": "הגדרות",
"features_title": "Features",
"export_calendar_button": "Export my calendar",
"upcoming_event_on_date": "Upcoming event on {date}",
"from_button": "מ",
"to_button": "עד",
"get_agenda_button": "Get Agenda",
"agenda_today": "היום",
"agenda_next_week": "שבוע הבא",
"agenda_next_month": "חודש הבא",
"agenda_invalid_date": "Start date is greater than end date",
"no_events_found": "לא נמצאו אירועים...",
"test_word": "בדיקה"
}
13 changes: 4 additions & 9 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@

from app.database import models
from app.database.database import engine
from app.dependencies import (
MEDIA_PATH, STATIC_PATH, templates)
from app.routers import agenda, event, profile, email

from app.dependencies import MEDIA_PATH, STATIC_PATH
from app.internal.utilities import get_template_response
from app.routers import agenda, email, event, profile

models.Base.metadata.create_all(bind=engine)

Expand All @@ -22,8 +21,4 @@

@app.get("/")
async def home(request: Request):
return templates.TemplateResponse("home.html", {
"request": request,
"message": "Hello, World!"

})
return get_template_response("home.html", request)
24 changes: 12 additions & 12 deletions app/routers/agenda.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
from sqlalchemy.orm import Session

from app.database.database import get_db
from app.dependencies import templates
from app.internal import agenda_events

from app.internal.utilities import get_template_response

router = APIRouter()

Expand All @@ -18,7 +17,7 @@ def calc_dates_range_for_agenda(
start: Optional[date],
end: Optional[date],
days: Optional[int]
) -> Tuple[date, date]:
) -> Tuple[date, date]:
"""Create start and end dates eccording to the parameters in the page."""
if days is not None:
start = date.today()
Expand All @@ -36,27 +35,28 @@ def agenda(
start_date: Optional[date] = None,
end_date: Optional[date] = None,
days: Optional[int] = None
) -> Jinja2Templates:
) -> Jinja2Templates:
"""Route for the agenda page, using dates range or exact amount of days."""

user_id = 1 # there is no user session yet, so I use user id- 1.
user_id = 1 # there is no user session yet, so I use user id- 1.
start_date, end_date = calc_dates_range_for_agenda(
start_date, end_date, days
)
)

events_objects = agenda_events.get_events_per_dates(
db, user_id, start_date, end_date
)
)
events = defaultdict(list)
for event_obj in events_objects:
event_duration = agenda_events.get_time_delta_string(
event_obj.start, event_obj.end
)
)
events[event_obj.start.date()].append((event_obj, event_duration))

return templates.TemplateResponse("agenda.html", {
"request": request,
variables = {
"events": events,
"start_date": start_date,
"end_date": end_date
})
"end_date": end_date,
}

return get_template_response("agenda.html", request, variables)
9 changes: 4 additions & 5 deletions app/routers/event.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Request

from app.dependencies import templates
from app.internal.utilities import get_template_response

router = APIRouter(
prefix="/event",
Expand All @@ -11,11 +11,10 @@

@router.get("/edit")
async def eventedit(request: Request):
return templates.TemplateResponse("event/eventedit.html",
{"request": request})
return get_template_response("event/eventedit.html", request)


@router.get("/view/{id}")
async def eventview(request: Request, id: int):
return templates.TemplateResponse("event/eventview.html",
{"request": request, "event_id": id})
variables = {"event_id": id}
return get_template_response("event/eventview.html", request, variables)
Loading