Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEEDS THOUGHT] added initial translation setup #295

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 2 additions & 11 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ repos:
- id: end-of-file-fixer
- id: check-yaml
- id: debug-statements
- id: double-quote-string-fixer
- id: name-tests-test
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.5.0
Expand All @@ -23,21 +22,13 @@ repos:
hooks:
- id: pyupgrade
args: [--py39-plus]
- repo: https://github.com/hhatto/autopep8
rev: v2.1.0
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
hooks:
- id: mypy
additional_dependencies: [types-all]
exclude: ^testing/resources/
exclude: ^(settings/|backend/)
- repo: https://github.com/psf/black
rev: 22.10.0
rev: 24.3.0
hooks:
- id: black
20 changes: 20 additions & 0 deletions backend/migrations/0031_user_language.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.0.4 on 2024-04-04 21:13
from __future__ import annotations

from django.db import migrations
from django.db import models


class Migration(migrations.Migration):

dependencies = [
("backend", "0030_alter_invoice_items"),
]

operations = [
migrations.AddField(
model_name="user",
name="language",
field=models.CharField(choices=[("en", "English"), ("ru", "Russian")], default="en-us", max_length=10),
),
]
38 changes: 25 additions & 13 deletions backend/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
from __future__ import annotations

from datetime import datetime
from decimal import Decimal
from typing import Optional, NoReturn, Union, Literal
from typing import Literal
from typing import NoReturn
from typing import Optional
from typing import Union
from uuid import uuid4

from django.contrib.auth.hashers import make_password, check_password
from django.contrib.auth.models import UserManager, AbstractUser, AnonymousUser
from django.contrib.auth.hashers import check_password
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import UserManager
from django.core.validators import MaxValueValidator
from django.db import models
from django.db.models import Count, QuerySet
from django.db.models import Count
from django.db.models import QuerySet
from django.utils import timezone
from django.utils.crypto import get_random_string
from shortuuid.django_fields import ShortUUIDField
Expand Down Expand Up @@ -37,7 +46,7 @@ def get_queryset(self):
super()
.get_queryset()
.select_related("user_profile", "logged_in_as_team")
.annotate(notification_count=((Count("user_notifications"))))
.annotate(notification_count=(Count("user_notifications")))
)


Expand All @@ -55,6 +64,7 @@ class Role(models.TextChoices):
TESTER = "TESTER", "Tester"

role = models.CharField(max_length=10, choices=Role.choices, default=Role.USER)
language = models.CharField(max_length=10, choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE)


class CustomUserMiddleware:
Expand Down Expand Up @@ -82,8 +92,10 @@ class ServiceTypes(models.TextChoices):
CREATE_ACCOUNT = "create_account", "Create Account"
RESET_PASSWORD = "reset_password", "Reset Password"

uuid = models.UUIDField(default=uuid4, editable=False, unique=True) # This is the public identifier
token = models.TextField(default=RandomCode, editable=False) # This is the private token (should be hashed)
# This is the public identifier
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
# This is the private token (should be hashed)
token = models.TextField(default=RandomCode, editable=False)

user = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
Expand Down Expand Up @@ -587,7 +599,7 @@ class Meta:
def __str__(self):
return self.name

def get_quota_limit(self, user: User, quota_limit: Optional["QuotaLimit"] = None):
def get_quota_limit(self, user: User, quota_limit: QuotaLimit | None = None):
try:
if quota_limit:
user_quota_override = quota_limit
Expand All @@ -607,14 +619,14 @@ def get_period_usage(self, user: User):
else:
return "Not available"

def strict_goes_above_limit(self, user: User, extra: Optional[str | int] = None) -> bool:
def strict_goes_above_limit(self, user: User, extra: str | int | None = None) -> bool:
current = self.strict_get_quotas(user, extra)
current = current.count() if current != "Not Available" else None
return current >= self.get_quota_limit(user) if current else False

def strict_get_quotas(
self, user: User, extra: Optional[str | int] = None, quota_limit: Optional["QuotaLimit"] = None
) -> Union['QuerySet["QuotaUsage"]', Literal["Not Available"]]:
self, user: User, extra: str | int | None = None, quota_limit: QuotaLimit | None = None
) -> QuerySet[QuotaUsage] | Literal["Not Available"]:
"""
Gets all usages of a quota
:return: QuerySet of quota usages OR "Not Available" if utilisation isn't available (e.g. per invoice you can't get in total)
Expand All @@ -640,7 +652,7 @@ def strict_get_quotas(
return current

@classmethod
def delete_quota_usage(cls, quota_limit: Union[str, "QuotaLimit"], user: User, extra, timestamp=None) -> NoReturn:
def delete_quota_usage(cls, quota_limit: str | QuotaLimit, user: User, extra, timestamp=None) -> NoReturn:
quota_limit = cls.objects.get(slug=quota_limit) if isinstance(quota_limit, str) else quota_limit

all_usages = quota_limit.strict_get_quotas(user, extra)
Expand Down Expand Up @@ -699,7 +711,7 @@ def __str__(self):
return f"{self.user} quota usage for {self.quota_limit_id}"

@classmethod
def create_str(cls, user: User, limit: str | QuotaLimit, extra_data: Optional[str | int] = None):
def create_str(cls, user: User, limit: str | QuotaLimit, extra_data: str | int | None = None):
try:
quota_limit = limit if isinstance(limit, QuotaLimit) else QuotaLimit.objects.get(slug=limit)
except QuotaLimit.DoesNotExist:
Expand Down
75 changes: 73 additions & 2 deletions docs/contributing/translations.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,73 @@
Currently we have no translations or a translation framework. If you have ideas for how we could implement translations feel
free to make an issue on github and suggest it for our docs!
Currently we support 2 languages: English and Russian.

## Translate non-translated strings
### Mark string as translatable
To make strings translatable we should use function `gettext_lazy`.
In python code:
```python
from django.utils.translation import gettext_lazy as _

print(_("Hello"))
```

In Django templates you may use `trans` from `i18n` module:
```html
{% load i18n %}
<!DOCTYPE html>
<html>
<body>
<div>{% trans "Hello" %}
</body>
</html>
```

### Scrape all messages to translate into *.po files
Run following command:
```shell
$ django-admin makemessages --all --ignore=env
```

Execute this command every time you mark new strings for translation or add new strings.

### Translate strings
Open file of the language you want to translate to (e.g. Russian): `locale/ru/LC_MESSAGES/django.po`
Translate every string (value in `msgstr`)
```python
#: frontend/templates/pages/index.html:24
msgid "Dashboard"
msgstr "Обзор"
```

### Compile strings for Django server
Run following command:
```shell
$ django-admin compilemessages --ignore=env
```

This command will generate `*.mo` files in `locale` folder. No need to include it into Git since it's a generated content (ecxcluded via .gitignore).
Execute this command every time you translate strings in `*.po` files.

## Adding new languages
1. Add new language into the `LANGUAGES` variable in `settings.py`:
```python
LANGUAGES = (
("en", _("English")),
("ru", _("Russian")),
("fr", _("French")) # <- e.g. this is a new language
)
```
2. Create new folder for you language
```shell
$ mkdir locale/fr
```
3. Collect new strings for your new language
```shell
$ django-admin makemessages --all --ignore=env
```
4. Translate strings in `*.po` files
1. Open in your favourite text editor
2. Or use GUI tools for translations (e.g. https://poedit.net/)
5. Compile strings into `*.mo` files
```shell
$ django-admin compilemessages --ignore=env
```
30 changes: 17 additions & 13 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,24 @@ git clone [copied fork url]
pip install poetry
poetry install --no-root
```
2. Setup a database (we suggest using sqlite so there's no installation!)
3. Migrate the database
```shell
python manage.py migrate
```
4. Create an administrator account
```shell
python manage.py createsuperuser
```
2. Setup a database (we suggest using sqlite so there's no installation!)
3. Compile translations
```shell
django-admin compilemessages --ignore=env
```
4. Migrate the database
```shell
python manage.py migrate
```
5. Create an administrator account
```shell
python manage.py createsuperuser
```

5. Run the application
```shell
python manage.py runserver
```
6. Run the application
```shell
python manage.py runserver
```

## Setup the frontend

Expand Down
15 changes: 8 additions & 7 deletions frontend/templates/pages/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="data()" lang="en">
{% include 'base/_head.html' %}
Expand All @@ -12,20 +13,20 @@
</a>
<div class="grow">
<a class="md:text-xl text-gray-800 font-semibold dark:text-gray-200"
href="/">My Finances</a>
href="/">{% trans "My Finances" %}</a>
</div>
</div>
</div>
</div>
<div class="navbar-end">
<div class="text-center sm:text-left flex sm:justify-end sm:items-center gap-x-3 md:gap-x-4">
{% if request.user.is_authenticated %}
<a class="btn btn-primary rounded-full" href="{% url "dashboard" %}">Dashboard</a>
<a class="btn btn-outline rounded-full" href="{% url 'auth:logout' %}">Logout</a>
<a class="btn btn-primary rounded-full" href="{% url "dashboard" %}">{% trans "Dashboard" %}</a>
<a class="btn btn-outline rounded-full" href="{% url 'auth:logout' %}">{% trans "Logout" %}</a>
{% else %}
<a class="btn btn-primary rounded-full"
href="{% url 'auth:login create_account' %}">Register</a>
<a class="btn btn-outline rounded-full " href="{% url 'auth:login' %}">Login</a>
href="{% url 'auth:login create_account' %}">{% trans "Register" %}</a>
<a class="btn btn-outline rounded-full " href="{% url 'auth:login' %}">{% trans "Login" %}</a>
{% endif %}
</div>
</div>
Expand All @@ -35,8 +36,8 @@
<main class="h-full w-full overflow-y-auto pb-16">
<div class="navbar-sticky">
<div class="text-4xl center text-center mt-8">
<p>We're still in development!</p>
<a class="btn btn-lg btn-primary mt-3" href="/dashboard/">Go to dashboard</a>
<p>{% trans "We're still in development!" %}</p>
<a class="btn btn-lg btn-primary mt-3" href="/dashboard/">{% trans "Go to dashboard" %}</a>
</div>
</div>
</main>
Expand Down
4 changes: 3 additions & 1 deletion infrastructure/backend/scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/bin/sh

python3 manage.py makemigrations --no-input && python3 manage.py migrate --no-input && python3 manage.py collectstatic --no-input
# compile translations
django-admin compilemessages --ignore=env

gunicorn settings.wsgi:application --bind 0.0.0.0:9012 --workers 2
gunicorn settings.wsgi:application --bind 0.0.0.0:9012 --workers 2
55 changes: 55 additions & 0 deletions locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-04 17:58-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: frontend/templates/pages/index.html:16
msgid "My Finances"
msgstr ""

#: frontend/templates/pages/index.html:24
msgid "Dashboard"
msgstr ""

#: frontend/templates/pages/index.html:25
msgid "Logout"
msgstr ""

#: frontend/templates/pages/index.html:28
msgid "Register"
msgstr ""

#: frontend/templates/pages/index.html:29
msgid "Login"
msgstr ""

#: frontend/templates/pages/index.html:39
msgid "We're still in development!"
msgstr ""

#: frontend/templates/pages/index.html:40
msgid "Go to dashboard"
msgstr ""

#: settings/settings.py:239
msgid "English"
msgstr ""

#: settings/settings.py:240
msgid "Russian"
msgstr ""
Loading