diff --git a/.gitignore b/.gitignore index ced5f7e..5bb154a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # cache **/__pycache__ +# translations +**.mo +**.pot + # config .vscode/ diff --git a/README.md b/README.md index 4963e96..e0f8a87 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ git clone https://github.com/partitioncloud/partitioncloud-server.git cd partitioncloud-server # Install dependencies pip install -r requirements.txt +pybabel compile -d partitioncloud/translations # Create database and folders ./make.sh init ``` @@ -66,6 +67,30 @@ Modifier le fichier de configuration créé dans `instance/` ![Recherche](https://github.com/partitioncloud/partitioncloud-server/assets/67148092/745bf3e3-37e9-40cd-80d2-14670bce1a45) +## Translations + +### Créer une nouvelle traduction + +```bash +# Extraire les données +pybabel extract -F babel.cfg -k _l -o partitioncloud/translations/messages.pot . +# Créer un nouveau fichier +pybabel init -i partitioncloud/translations/messages.pot -d partitioncloud/translations/ -l $COUNTRY_CODE +# Modifier translations/$COUNTRY_CODE/LC_MESSAGES/messages.po +# Ajouter $COUNTRY_CODE dans default_config.py: LANGUAGES +# Compiler les nouvelles translations avant de démarrer le serveur +pybabel compile -d partitioncloud/translations/ +``` + +### Mettre à jour une traduction + +```bash +# Récupérer les données les plus récentes +pybabel extract -F babel.cfg -k _l -o partitioncloud/translations/messages.pot . +# Les ajouter aux traductions +pybabel update -i partitioncloud/translations/messages.pot -d partitioncloud/translations/ +``` + ## TODO - [ ] Modifier son mot de passe - [ ] Supprimer un utilisateur diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..97ebeb8 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,2 @@ +[python: partitioncloud/**.py] +[jinja2: partitioncloud/templates/**.html] \ No newline at end of file diff --git a/default_config.py b/default_config.py index 253e482..d13ae2e 100644 --- a/default_config.py +++ b/default_config.py @@ -26,4 +26,7 @@ MAX_AGE=31 INSTANCE_PATH="instance" # Events to log -ENABLED_LOGS=["NEW_GROUPE", "NEW_ALBUM", "NEW_PARTITION", "NEW_USER", "SERVER_RESTART", "FAILED_LOGIN"] \ No newline at end of file +ENABLED_LOGS=["NEW_GROUPE", "NEW_ALBUM", "NEW_PARTITION", "NEW_USER", "SERVER_RESTART", "FAILED_LOGIN"] + +# Available languages +LANGUAGES=['en', 'fr'] \ No newline at end of file diff --git a/make.sh b/make.sh index 566b605..7f22af3 100755 --- a/make.sh +++ b/make.sh @@ -25,30 +25,50 @@ init () { echo "Utilisateur root:root ajouté" } +translations () { + # Rajouter les chaînes non traduites + pybabel extract -F babel.cfg -k _l -o partitioncloud/translations/messages.pot . + pybabel update -i partitioncloud/translations/messages.pot -d partitioncloud/translations/ + # Compiler + pybabel compile -d partitioncloud/translations/ +} + start () { + pybabel compile -d partitioncloud/translations/ flask run --port=$PORT } production () { + pybabel compile -d partitioncloud/translations/ FLASK_APP=partitioncloud /usr/bin/gunicorn \ wsgi:app \ --bind 0.0.0.0:$PORT } +load_config () { + # Load variables PORT and INSTANCE_PATH + eval $(cat $1 | grep -E "^PORT=") + eval $(cat $1 | grep -E "^INSTANCE_PATH=") +} + usage () { echo "Usage:" echo -e "\t$0 init" echo -e "\t$0 start" + echo -e "\t$0 production" + echo -e "\t$0 translations" } -if [[ $1 && $(type "$1") = *"is a"*"function"* || $(type "$1") == *"est une fonction"* ]]; then + +RESULT=$(type "$1") +if [[ $1 && $RESULT = *"is a"*"function"* || $RESULT == *"est une fonction"* ]]; then # Import config - source "default_config.py" - [[ ! -x "$INSTANCE_PATH/config.py" ]] && source "$INSTANCE_PATH/config.py" + load_config "default_config.py" + [[ ! -x "$INSTANCE_PATH/config.py" ]] && load_config "$INSTANCE_PATH/config.py" $1 ${*:2} # Call the function else usage - echo $(type "$1") + echo $RESULT exit 1 fi diff --git a/partitioncloud/__init__.py b/partitioncloud/__init__.py index dacea5f..afbf031 100644 --- a/partitioncloud/__init__.py +++ b/partitioncloud/__init__.py @@ -10,6 +10,7 @@ import importlib.util from flask import Flask, g, redirect, render_template, request, send_file, flash, session, abort from werkzeug.security import generate_password_hash +from flask_babel import Babel, _ from .modules.utils import User, Album, get_all_albums from .modules import albums, auth, partition, admin, groupe, thumbnails, logging @@ -18,6 +19,12 @@ from .modules.db import get_db app = Flask(__name__) +def get_locale(): + return request.accept_languages.best_match(app.config['LANGUAGES']) + +babel = Babel(app, locale_selector=get_locale) + + def load_config(): app.config.from_object('default_config') @@ -125,10 +132,10 @@ def add_user(): try: if album_uuid != "": user.join_album(album_uuid) - flash(f"Utilisateur {username} créé") + flash(_("Created user %(username)s", username=username)) return redirect("/albums") except LookupError: - flash(f"Cet album n'existe pas. L'utilisateur {username} a été créé") + flash(_("This album does not exists, but user %(username)s has been created", username=username)) return redirect("/albums") flash(error) diff --git a/partitioncloud/modules/albums.py b/partitioncloud/modules/albums.py index 40ca12a..7cb71a4 100644 --- a/partitioncloud/modules/albums.py +++ b/partitioncloud/modules/albums.py @@ -9,6 +9,7 @@ from typing import TypeVar from flask import (Blueprint, abort, flash, redirect, render_template, request, session, current_app) +from flask_babel import _ from .auth import login_required from .db import get_db @@ -37,7 +38,7 @@ def search_page(): Résultats de recherche """ if "query" not in request.form or request.form["query"] == "": - flash("Aucun terme de recherche spécifié.") + flash(_("Missing search query")) return redirect("/albums") query = request.form["query"] @@ -119,7 +120,7 @@ def create_album_req(): user = User(user_id=session["user_id"]) if not name or name.strip() == "": - error = "Un nom est requis. L'album n'a pas été créé" + error = _("Missing name.") if error is None: uuid = utils.create_album(name) @@ -156,10 +157,10 @@ def join_album(uuid): try: user.join_album(uuid) except LookupError: - flash("Cet album n'existe pas.") + flash(_("This album does not exist.")) return redirect(request.referrer) - flash("Album ajouté à la collection.") + flash(_("Album added to collection.")) return redirect(request.referrer) @@ -173,15 +174,15 @@ def quit_album(uuid): album = Album(uuid=uuid) users = album.get_users() if user.id not in [u["id"] for u in users]: - flash("Vous ne faites pas partie de cet album") + flash(_("You are not a member of this album")) return redirect(request.referrer) if len(users) == 1: - flash("Vous êtes seul dans cet album, le quitter entraînera sa suppression.") + flash(_("You are alone here, quitting means deleting this album.")) return redirect(f"/albums/{uuid}#delete") user.quit_album(uuid) - flash("Album quitté.") + flash(_("Album quitted.")) return redirect("/albums") @@ -200,9 +201,9 @@ def delete_album(uuid): error = None users = album.get_users() if len(users) > 1: - error = "Vous n'êtes pas seul dans cet album." + error = _("You are not alone in this album.") elif len(users) == 1 and users[0]["id"] != user.id: - error = "Vous ne possédez pas cet album." + error = _("You don't own this album.") if user.access_level == 1: error = None @@ -213,7 +214,7 @@ def delete_album(uuid): album.delete(current_app.instance_path) - flash("Album supprimé.") + flash(_("Album deleted.")) return redirect("/albums") @@ -236,15 +237,15 @@ def add_partition(album_uuid): source = "upload" # source type: upload, unknown or url if (not user.is_participant(album.uuid)) and (user.access_level != 1): - flash("Vous ne participez pas à cet album.") + flash(_("You are not a member of this album")) return redirect(request.referrer) error = None if "name" not in request.form: - error = "Un titre est requis." + error = _("Missing title") elif "file" not in request.files and "partition-uuid" not in request.form: - error = "Aucun fichier n'a été fourni." + error = _("Missing file") elif "file" not in request.files: partition_type = "uuid" search_uuid = request.form["partition-uuid"] @@ -256,7 +257,7 @@ def add_partition(album_uuid): (search_uuid,) ).fetchone() if data is None: - error = "Les résultats de la recherche ont expiré." + error = _("Search results expired") else: source = data["url"] else: @@ -322,7 +323,7 @@ def add_partition(album_uuid): "status": "ok", "uuid": partition_uuid } - flash(f"Partition {request.form['name']} ajoutée") + flash(_("Score %(partition_name)s added", partition_name=request.form['name'])) return redirect(f"/albums/{album.uuid}") @@ -336,13 +337,13 @@ def add_partition_from_search(): error = None if "album-uuid" not in request.form: - error = "Il est nécessaire de sélectionner un album." + error = _("Selecting an album is mandatory.") elif "partition-uuid" not in request.form: - error = "Il est nécessaire de sélectionner une partition." + error = _("Selecting a score is mandatory.") elif "partition-type" not in request.form: - error = "Il est nécessaire de spécifier un type de partition." + error = _("Please specify a score type.") elif (not user.is_participant(request.form["album-uuid"])) and (user.access_level != 1): - error = "Vous ne participez pas à cet album." + error = _("You are not a member of this album") if error is not None: flash(error) @@ -362,9 +363,9 @@ def add_partition_from_search(): if data is None: album.add_partition(request.form["partition-uuid"]) - flash("Partition ajoutée.") + flash(_("Score added")) else: - flash("Partition déjà dans l'album.") + flash(_("Score is already in the album.")) return redirect(f"/albums/{album.uuid}") @@ -376,5 +377,5 @@ def add_partition_from_search(): user=user ) - flash("Type de partition inconnu.") + flash(_("Unknown score type.")) return redirect("/albums") diff --git a/partitioncloud/modules/auth.py b/partitioncloud/modules/auth.py index 8d08c6e..21c5cf0 100644 --- a/partitioncloud/modules/auth.py +++ b/partitioncloud/modules/auth.py @@ -7,6 +7,7 @@ from typing import Optional from flask import (Blueprint, flash, g, redirect, render_template, request, session, url_for, current_app) +from flask_babel import _ from werkzeug.security import check_password_hash, generate_password_hash @@ -23,7 +24,7 @@ def login_required(view): @functools.wraps(view) def wrapped_view(**kwargs): if g.user is None: - flash("Vous devez être connecté pour accéder à cette page.") + flash(_("You need to login to access this resource.")) return redirect(url_for("auth.login")) return view(**kwargs) @@ -50,12 +51,12 @@ def admin_required(view): @functools.wraps(view) def wrapped_view(**kwargs): if g.user is None: - flash("Vous devez être connecté pour accéder à cette page.") + flash(_("You need to login to access this resource.")) return redirect(url_for("auth.login")) user = User(user_id=session.get("user_id")) if user.access_level != 1: - flash("Droits insuffisants.") + flash(_("Missing rights.")) return redirect("/albums") return view(**kwargs) @@ -81,9 +82,9 @@ def create_user(username: str, password: str) -> Optional[str]: """Adds a new user to the database""" error = None if not username: - error = "Un nom d'utilisateur est requis." + error = _("Missing username.") elif not password: - error = "Un mot de passe est requis." + error = _("Missing password.") try: db = get_db() @@ -96,7 +97,7 @@ def create_user(username: str, password: str) -> Optional[str]: except db.IntegrityError: # The username was already taken, which caused the # commit to fail. Show a validation error. - error = f"Le nom d'utilisateur {username} est déjà pris." + error = _("Username %(username)s is not available.", username=username) return error # may be None @@ -109,7 +110,7 @@ def register(): password for security. """ if current_app.config["DISABLE_REGISTER"]: - flash("L'enregistrement de nouveaux utilisateurs a été désactivé par l'administrateur.") + flash(_("New users registration is disabled by owner.")) return redirect(url_for("auth.login")) if request.method == "POST": @@ -123,7 +124,7 @@ def register(): else: user = User(name=username) - flash("Utilisateur créé avec succès. Vous pouvez vous connecter.") + flash(_("Successfully created new user. You can log in.")) logging.log( [user.username, user.id, False], @@ -148,7 +149,7 @@ def login(): if (user is None) or not check_password_hash(user["password"], password): logging.log([username], logging.LogEntry.FAILED_LOGIN) - error = "Nom d'utilisateur ou mot de passe incorrect." + error = _("Incorrect username or password") if error is None: # store the user id in a new session and return to the index diff --git a/partitioncloud/modules/groupe.py b/partitioncloud/modules/groupe.py index 6991f3a..92bcd2d 100644 --- a/partitioncloud/modules/groupe.py +++ b/partitioncloud/modules/groupe.py @@ -4,6 +4,7 @@ Groupe module """ from flask import (Blueprint, abort, flash, redirect, render_template, request, session, current_app) +from flask_babel import _ from .auth import login_required from .db import get_db @@ -67,7 +68,7 @@ def create_groupe(): user = User(user_id=session["user_id"]) if not name or name.strip() == "": - error = "Un nom est requis. Le groupe n'a pas été créé" + error = _("Missing name.") if error is None: while True: @@ -116,10 +117,10 @@ def join_groupe(uuid): try: user.join_groupe(uuid) except LookupError: - flash("Ce groupe n'existe pas.") + flash(_("Unknown group.")) return redirect(f"/groupe/{uuid}") - flash("Groupe ajouté à la collection.") + flash(_("Group added to collection.")) return redirect(f"/groupe/{uuid}") @@ -130,15 +131,15 @@ def quit_groupe(uuid): groupe = Groupe(uuid=uuid) users = groupe.get_users() if user.id not in [u["id"] for u in users]: - flash("Vous ne faites pas partie de ce groupe") + flash(_("You are not a member of this group.")) return redirect(f"/groupe/{uuid}") if len(users) == 1: - flash("Vous êtes seul dans ce groupe, le quitter entraînera sa suppression.") + flash(_("You are alone here, quitting means deleting this group.")) return redirect(f"/groupe/{uuid}#delete") user.quit_groupe(groupe.uuid) - flash("Groupe quitté.") + flash(_("Group quitted.")) return redirect("/albums") @@ -151,7 +152,7 @@ def delete_groupe(uuid): error = None users = groupe.get_users() if len(users) > 1: - error = "Vous n'êtes pas seul dans ce groupe." + error = _("You are not alone in this group.") if user.access_level == 1 or user.id not in groupe.get_admins(): error = None @@ -162,7 +163,7 @@ def delete_groupe(uuid): groupe.delete(current_app.instance_path) - flash("Groupe supprimé.") + flash(_("Group deleted.")) return redirect("/albums") @@ -181,10 +182,10 @@ def create_album_req(groupe_uuid): error = None if not name or name.strip() == "": - error = "Un nom est requis. L'album n'a pas été créé" + error = _("Missing name.") if user.id not in groupe.get_admins(): - error ="Vous n'êtes pas administrateur de ce groupe" + error = _("You are not admin of this group.") if error is None: uuid = utils.create_album(name) diff --git a/partitioncloud/modules/logging.py b/partitioncloud/modules/logging.py index 472022e..162c4a5 100644 --- a/partitioncloud/modules/logging.py +++ b/partitioncloud/modules/logging.py @@ -30,7 +30,7 @@ class LogEntry(Enum): def add_entry(entry: str) -> None: - date = datetime.now().strftime("%y-%b-%Y %H:%M:%S") + date = datetime.now().strftime('%y-%b-%Y %H:%M:%S') with open(log_file, 'a', encoding="utf8") as f: f.write(f"[{date}] {entry}\n") diff --git a/partitioncloud/modules/partition.py b/partitioncloud/modules/partition.py index 700b221..d560f0c 100644 --- a/partitioncloud/modules/partition.py +++ b/partitioncloud/modules/partition.py @@ -6,6 +6,7 @@ import os from uuid import uuid4 from flask import (Blueprint, abort, send_file, render_template, request, redirect, flash, session, current_app) +from flask_babel import _ from .db import get_db from .auth import login_required, admin_required @@ -54,12 +55,12 @@ def add_attachment(uuid): user = User(user_id=session.get("user_id")) if user.id != partition.user_id and user.access_level != 1: - flash("Cette partition ne vous current_appartient pas") + flash(_("You don't own this score.")) return redirect(request.referrer) error = None # À mettre au propre if "file" not in request.files: - error = "Aucun fichier n'a été fourni." + error = _("Missing file") else: if "name" not in request.form or request.form["name"] == "": name = ".".join(request.files["file"].filename.split(".")[:-1]) @@ -67,12 +68,12 @@ def add_attachment(uuid): name = request.form["name"] if name == "": - error = "Pas de nom de fichier" + error = _("Missing filename.") else: filename = request.files["file"].filename ext = filename.split(".")[-1] if ext not in ["mid", "mp3"]: - error = "Extension de fichier non supportée" + error = _("Unsupported file type.") if error is not None: flash(error) @@ -140,7 +141,7 @@ def edit(uuid): user = User(user_id=session.get("user_id")) if user.access_level != 1 and partition.user_id != user.id: - flash("Vous n'êtes pas autorisé à modifier cette partition.") + flash(_("You are not allowed to edit this file.")) return redirect("/albums") if request.method == "GET": @@ -149,11 +150,11 @@ def edit(uuid): error = None if "name" not in request.form or request.form["name"].strip() == "": - error = "Un titre est requis." + error = _("Missing title") elif "author" not in request.form: - error = "Un nom d'auteur est requis (à minima nul)" + error = _("Missing author in request body (can be null).") elif "body" not in request.form: - error = "Des paroles sont requises (à minima nulles)" + error = _("Missing lyrics (can be null).") if error is not None: flash(error) @@ -165,7 +166,7 @@ def edit(uuid): body=request.form["body"] ) - flash(f"Partition {request.form['name']} modifiée avec succès.") + flash(_("Successfully modified %(name)s", name=request.form['name'])) return redirect("/albums") @@ -195,11 +196,11 @@ def details(uuid): error = None if "name" not in request.form or request.form["name"].strip() == "": - error = "Un titre est requis." + error = _("Missing title") elif "author" not in request.form: - error = "Un nom d'auteur est requis (à minima nul)" + error = _("Missing author in request body (can be null).") elif "body" not in request.form: - error = "Des paroles sont requises (à minima nulles)" + error = _("Missing lyrics (can be null).") if error is not None: flash(error) @@ -211,7 +212,7 @@ def details(uuid): body=request.form["body"] ) - flash(f"Partition {request.form['name']} modifiée avec succès.") + flash(_("Successfully modified %(name)s", name=request.form['name'])) return redirect("/albums") @@ -226,7 +227,7 @@ def delete(uuid): user = User(user_id=session.get("user_id")) if user.access_level != 1 and partition.user_id != user.id: - flash("Vous n'êtes pas autorisé à supprimer cette partition.") + flash(_("You are not allowed to delete this score.")) return redirect("/albums") if request.method == "GET": @@ -234,7 +235,7 @@ def delete(uuid): partition.delete(current_app.instance_path) - flash("Partition supprimée.") + flash(_("Score deleted.")) return redirect("/albums") diff --git a/partitioncloud/templates/admin/index.html b/partitioncloud/templates/admin/index.html index 6dcb070..dff4f68 100644 --- a/partitioncloud/templates/admin/index.html +++ b/partitioncloud/templates/admin/index.html @@ -2,27 +2,27 @@ {% block content %} -
Utilisateur | -Albums | -Partitions | -Privilèges | +{{ _("User") }} | +{{ _("Albums") }} | +{{ _("Scores") }} | +{{ _("Admin privileges") }} | 🎵 {{ attachment.name }} | {% endif %} @@ -57,7 +57,7 @@
---|