From 0f8b20c9dc2799259a16a2b1ce9bcdb6e16ec2af Mon Sep 17 00:00:00 2001 From: Gonny <1@1> Date: Fri, 22 Jan 2021 12:04:32 +0200 Subject: [PATCH 01/53] 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. --- app/database/models.py | 9 ++++- app/internal/languages.py | 80 +++++++++++++++++++++++++++++++++++++++ app/languages/en.json | 30 +++++++++++++++ app/languages/he.json | 30 +++++++++++++++ app/main.py | 15 +++----- app/routers/agenda.py | 24 ++++++------ app/routers/event.py | 11 ++++-- app/routers/profile.py | 21 +++++----- app/templates/agenda.html | 14 +++---- app/templates/base.html | 14 +++---- app/templates/home.html | 1 - requirements.txt | 24 ++++++++++-- tests/test_language.py | 47 +++++++++++++++++++++++ 13 files changed, 266 insertions(+), 54 deletions(-) create mode 100644 app/internal/languages.py create mode 100644 app/languages/en.json create mode 100644 app/languages/he.json create mode 100644 tests/test_language.py diff --git a/app/database/models.py b/app/database/models.py index 0c92ae94..23106fb4 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -14,7 +14,7 @@ class User(Base): full_name = Column(String) description = Column(String, default="Happy new user!") avatar = Column(String, default="profile.png") - + language = Column(Integer, ForeignKey("languages.id")) is_active = Column(Boolean, default=True) events = relationship( @@ -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) diff --git a/app/internal/languages.py b/app/internal/languages.py new file mode 100644 index 00000000..83b453e8 --- /dev/null +++ b/app/internal/languages.py @@ -0,0 +1,80 @@ +import glob +import json +from typing import Dict, Union + +APP_LANGUAGE = "en" + +LANGUAGE_FILES_PATH = "app/languages/*.json" +LANGUAGE_FILES_PATH_TEST = "../app/languages/*.json" + +translations_dict = {} + + +def get_translations_dict() -> Dict[str, Union[str, Dict[str, str]]]: + """Gets and returns the translations_dict, which is a dictionary of the translated words + in either the user's language setting, or the default app setting. + + 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. + """ + if translations_dict: + return translations_dict + # TODO: Waiting for user registration. Restore when done. + # display_language = get_display_language(user_id) + # update_translations_dict(display_language) + update_translations_dict(APP_LANGUAGE) + return translations_dict + + +# 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 APP_LANGUAGE + + +def update_translations_dict(display_language: str) -> None: + """Updates the translations_dict to the requested language. + If the language code is not supported by the applications, the dictionary defaults to the APP_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. + + .. _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 + """ + translations_dicts = get_translations_dicts() + global translations_dict + if display_language in translations_dicts: + translations_dict = translations_dicts[display_language] + else: + translations_dict = translations_dicts[APP_LANGUAGE] + + +def get_translations_dicts() -> 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. + """ + supported_languages = {} + language_list = glob.glob(LANGUAGE_FILES_PATH) + if not language_list: + language_list = glob.glob(LANGUAGE_FILES_PATH_TEST) # Running from tests. + for lang in language_list: + filename = lang.split('\\') + lang_code = filename[1].split('.')[0] + + with open(lang, 'r', encoding='utf8') as file: + supported_languages[lang_code] = json.load(file) + return supported_languages diff --git a/app/languages/en.json b/app/languages/en.json new file mode 100644 index 00000000..9d32c4e0 --- /dev/null +++ b/app/languages/en.json @@ -0,0 +1,30 @@ +{ + "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", + "no_events_found": "No events found...", + "test_word": "test" +} \ No newline at end of file diff --git a/app/languages/he.json b/app/languages/he.json new file mode 100644 index 00000000..aa0efa8c --- /dev/null +++ b/app/languages/he.json @@ -0,0 +1,30 @@ +{ + "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": "חודש הבא", + "no_events_found": "לא נמצאו אירועים...", + "test_word": "בדיקה" +} \ No newline at end of file diff --git a/app/main.py b/app/main.py index 07861586..852c7fd5 100644 --- a/app/main.py +++ b/app/main.py @@ -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, templates +from app.internal import languages +from app.routers import agenda, email, event, profile models.Base.metadata.create_all(bind=engine) @@ -22,8 +21,6 @@ @app.get("/") async def home(request: Request): - return templates.TemplateResponse("home.html", { - "request": request, - "message": "Hello, World!" - - }) + result = {"request": request} + result.update(languages.get_translations_dict()) + return templates.TemplateResponse("home.html", result) diff --git a/app/routers/agenda.py b/app/routers/agenda.py index f8fd532b..d0d8bc1f 100644 --- a/app/routers/agenda.py +++ b/app/routers/agenda.py @@ -8,8 +8,7 @@ from app.database.database import get_db from app.dependencies import templates -from app.internal import agenda_events - +from app.internal import agenda_events, languages router = APIRouter() @@ -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() @@ -36,27 +35,30 @@ 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", { + result = { "request": request, "events": events, "start_date": start_date, - "end_date": end_date - }) + "end_date": end_date, + } + + result.update(languages.get_translations_dict()) + return templates.TemplateResponse("agenda.html", result) diff --git a/app/routers/event.py b/app/routers/event.py index f2a0b2dc..0061d189 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Request from app.dependencies import templates +from app.internal import languages router = APIRouter( prefix="/event", @@ -11,11 +12,13 @@ @router.get("/edit") async def eventedit(request: Request): - return templates.TemplateResponse("event/eventedit.html", - {"request": request}) + result = {"request": request} + result.update(languages.get_translations_dict()) + return templates.TemplateResponse("event/eventedit.html", result) @router.get("/view/{id}") async def eventview(request: Request, id: int): - return templates.TemplateResponse("event/eventview.html", - {"request": request, "event_id": id}) + result = {"request": request, "event_id": id} + result.update(languages.get_translations_dict()) + return templates.TemplateResponse("event/eventview.html", result) diff --git a/app/routers/profile.py b/app/routers/profile.py index 39724939..1bd1163d 100644 --- a/app/routers/profile.py +++ b/app/routers/profile.py @@ -1,15 +1,15 @@ import io from fastapi import APIRouter, Depends, File, Request, UploadFile +from PIL import Image from starlette.responses import RedirectResponse from starlette.status import HTTP_302_FOUND -from PIL import Image from app import config from app.database.database import get_db from app.database.models import User from app.dependencies import MEDIA_PATH, templates - +from app.internal import languages PICTURE_EXTENSION = config.PICTURE_EXTENSION PICTURE_SIZE = config.AVATAR_SIZE @@ -35,9 +35,8 @@ async def profile( request: Request, session=Depends(get_db), new_user=Depends(get_placeholder_user)): - # Get relevant data from database - upcouming_events = range(5) + upcoming_events = range(5) user = session.query(User).filter_by(id=1).first() if not user: session.add(new_user) @@ -46,17 +45,20 @@ async def profile( session.close() - return templates.TemplateResponse("profile.html", { + result = { "request": request, "user": user, - "events": upcouming_events - }) + "events": upcoming_events, + } + + result.update(languages.get_translations_dict()) + + return templates.TemplateResponse("profile.html", result) @router.post("/update_user_fullname") async def update_user_fullname( request: Request, session=Depends(get_db)): - user = session.query(User).filter_by(id=1).first() data = await request.form() new_fullname = data['fullname'] @@ -75,7 +77,6 @@ async def update_user_fullname( @router.post("/update_user_email") async def update_user_email( request: Request, session=Depends(get_db)): - user = session.query(User).filter_by(id=1).first() data = await request.form() new_email = data['email'] @@ -93,7 +94,6 @@ async def update_user_email( @router.post("/update_user_description") async def update_profile( request: Request, session=Depends(get_db)): - user = session.query(User).filter_by(id=1).first() data = await request.form() new_description = data['description'] @@ -111,7 +111,6 @@ async def update_profile( @router.post("/upload_user_photo") async def upload_user_photo( file: UploadFile = File(...), session=Depends(get_db)): - user = session.query(User).filter_by(id=1).first() pic = await file.read() diff --git a/app/templates/agenda.html b/app/templates/agenda.html index 873b69e5..51ab93e8 100644 --- a/app/templates/agenda.html +++ b/app/templates/agenda.html @@ -8,22 +8,22 @@ {% block content %}
-
+

-
+

- +
- Today + {{ agenda_today }}
- Next Week + {{ agenda_next_week }}
- Next Month + {{ agenda_next_month }}
@@ -32,7 +32,7 @@ {% if start_date > end_date %}

Start date is greater than end date

{% elif events | length == 0 %} -

No events found...

+

{{ no_events_found }}

{% elif start_date == end_date %}

{{ start_date.strftime("%d/%m/%Y") }}

{% else %} diff --git a/app/templates/base.html b/app/templates/base.html index ddc549db..dcaada50 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -9,13 +9,13 @@ integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> - Calendar + {{ website_title }} {% endblock %}