diff --git a/.gitignore b/.gitignore index fd25cf7..d9b4df4 100644 --- a/.gitignore +++ b/.gitignore @@ -89,7 +89,7 @@ coverage.xml cover/ # Translations -*.mo +*.po *.pot # Django stuff: diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..efceab8 --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/bin/translate.py b/bin/translate.py new file mode 100755 index 0000000..93d6644 --- /dev/null +++ b/bin/translate.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import sys + +import polib +from deep_translator import GoogleTranslator + + +def main(filename: str, language: str) -> None: + po = polib.pofile(filename) + translator = GoogleTranslator(source="en", target=language) + + for entry in po: + entry.msgstr = translator.translate(entry.msgid) + + po.save(filename) + + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/bin/translate.sh b/bin/translate.sh new file mode 100755 index 0000000..af762ac --- /dev/null +++ b/bin/translate.sh @@ -0,0 +1,11 @@ +#!/bin/sh +uv run pybabel extract -F babel.cfg -o messages.pot . +uv run pybabel init -i messages.pot -d translations -l es +uv run pybabel init -i messages.pot -d translations -l it +uv run pybabel init -i messages.pot -d translations -l fr +echo "Translating..." +uv run ./bin/translate.py translations/es/LC_MESSAGES/messages.po es +uv run ./bin/translate.py translations/it/LC_MESSAGES/messages.po it +uv run ./bin/translate.py translations/fr/LC_MESSAGES/messages.po fr +uv run pybabel compile -d translations +echo "Done." diff --git a/pyproject.toml b/pyproject.toml index 8930e4f..8d91bc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,8 +28,9 @@ dependencies = [ "pygit2==1.17.0", "textual==3.1.0", "pygithub==2.6.1", - "pyyaml>=6.0", - "platformdirs>=4.3.7", + "pyyaml==6.0.2", + "platformdirs==4.3.7", + "babel==2.17.0", ] [project.urls] @@ -51,9 +52,11 @@ path = "src/edit_python_pe/__about__.py" [dependency-groups] dev = [ "black>=25.1.0", + "deep-translator>=1.11.4", "isort>=6.0.1", + "polib>=1.2.0", "pytest>=8.4.1", - "textual-dev>=1.7.0", + "textual-dev==1.7.0", ] [tool.black] diff --git a/src/edit_python_pe/constants.py b/src/edit_python_pe/constants.py new file mode 100644 index 0000000..c6c0d04 --- /dev/null +++ b/src/edit_python_pe/constants.py @@ -0,0 +1,15 @@ +# Locales +EN_LOCALE = "en" +ES_LOCALE = "es" +IT_LOCALE = "it" +FR_LOCALE = "fr" + +# Social network options +GITHUB_OPTION = ("GitHub", "github") +GITLAB_OPTION = ("GitLab", "gitlab") +BITBUCKET_OPTION = ("Bitbucket", "bitbucket") +LINKEDIN_OPTION = ("LinkedIn", "linkedin") +FACEBOOK_OPTION = ("Facebook", "facebook") +INSTAGRAM_OPTION = ("Instagram", "instagram") +X_OPTION = ("X", "x") +YOUTUBE_OPTION = ("YouTube", "youtube") diff --git a/src/edit_python_pe/main.py b/src/edit_python_pe/main.py index 69398ec..5f12fd0 100755 --- a/src/edit_python_pe/main.py +++ b/src/edit_python_pe/main.py @@ -8,14 +8,17 @@ from textual.widgets import (Button, Input, ListItem, ListView, Select, Static, TextArea) +from .constants import (BITBUCKET_OPTION, FACEBOOK_OPTION, GITHUB_OPTION, + GITLAB_OPTION, INSTAGRAM_OPTION, LINKEDIN_OPTION, + X_OPTION, YOUTUBE_OPTION) from .strings import (BUTTON_ADD, BUTTON_ADD_ALIAS, BUTTON_ADD_SOCIAL, BUTTON_BACK, BUTTON_DELETE, BUTTON_QUIT, BUTTON_SAVE, - FORM_HEADER, LIST_TITLE, MESSAGE_EXIT, MESSAGE_QUIT, - PLACEHOLDER_ALIAS, PLACEHOLDER_CITY, PLACEHOLDER_EMAIL, + FORM_HEADER, LIST_TITLE, MESSAGE_EXIT, PLACEHOLDER_ALIAS, + PLACEHOLDER_CITY, PLACEHOLDER_EMAIL, PLACEHOLDER_HOMEPAGE, PLACEHOLDER_NAME, - PLACEHOLDER_SOCIAL_URL, SECTION_ALIASES, SECTION_AVAIL, - SECTION_CONTRIB, SECTION_PYTHON, SECTION_SOCIAL, - SECTION_WHO) + PLACEHOLDER_SOCIAL_URL, PROMPT_SOCIAL_NETWORK, + SECTION_ALIASES, SECTION_AVAIL, SECTION_CONTRIB, + SECTION_PYTHON, SECTION_SOCIAL, SECTION_WHO) from .utils import (build_md_content, create_pr, fork_repo, get_repo, load_file_into_form) @@ -195,7 +198,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: self.clear_form() self.show_list() elif bid == "quit": - self.exit(MESSAGE_QUIT) + self.exit(message=MESSAGE_EXIT) elif bid and bid.startswith("delete_social_"): index = int(bid.replace("delete_social_", "")) self.remove_social_entry(index) @@ -222,16 +225,16 @@ def __init__(se, index): se.index = index se.select = Select( options=[ - ("GitHub", "github"), - ("GitLab", "gitlab"), - ("Bitbucket", "bitbucket"), - ("LinkedIn", "linkedin"), - ("Facebook", "facebook"), - ("Instagram", "instagram"), - ("X", "x"), - ("YouTube", "youtube"), + GITHUB_OPTION, + GITLAB_OPTION, + BITBUCKET_OPTION, + LINKEDIN_OPTION, + FACEBOOK_OPTION, + INSTAGRAM_OPTION, + X_OPTION, + YOUTUBE_OPTION, ], - prompt="Social Network", + prompt=PROMPT_SOCIAL_NETWORK, ) se.url_input = Input(placeholder=PLACEHOLDER_SOCIAL_URL) se.delete_btn = Button( diff --git a/src/edit_python_pe/strings.py b/src/edit_python_pe/strings.py index f647083..3008c7e 100644 --- a/src/edit_python_pe/strings.py +++ b/src/edit_python_pe/strings.py @@ -1,56 +1,75 @@ +import gettext +import locale +from pathlib import Path + +from .constants import EN_LOCALE, ES_LOCALE, FR_LOCALE, IT_LOCALE + +default_locale = locale.getlocale()[0] or EN_LOCALE +localedir = Path(__file__).parent.parent / "translations" +_ = gettext.translation( + domain="messages", + localedir=localedir, + languages=[default_locale, ES_LOCALE, IT_LOCALE, FR_LOCALE], + fallback=True, +).gettext + # Field, control, and message labels for edit_python_pe # List and form titles -LIST_TITLE = "Files in 'blog/members':" -FORM_HEADER = "Member Form" +LIST_TITLE = _("Files in 'blog/members':") +FORM_HEADER = _("Member Form") # Button labels -BUTTON_QUIT = "Quit" -BUTTON_ADD = "Add" -BUTTON_SAVE = "Save" -BUTTON_BACK = "Back" -BUTTON_ADD_SOCIAL = "Add Social Network" -BUTTON_ADD_ALIAS = "Add Alias" -BUTTON_DELETE = "Delete" +BUTTON_QUIT = _("Quit") +BUTTON_ADD = _("Add") +BUTTON_SAVE = _("Save") +BUTTON_BACK = _("Back") +BUTTON_ADD_SOCIAL = _("Add Social Network") +BUTTON_ADD_ALIAS = _("Add Alias") +BUTTON_DELETE = _("Delete") # Input placeholders -PLACEHOLDER_NAME = "Name" -PLACEHOLDER_EMAIL = "Email" -PLACEHOLDER_CITY = "City" -PLACEHOLDER_HOMEPAGE = "Homepage" -PLACEHOLDER_SOCIAL_URL = "Social network URL" -PLACEHOLDER_ALIAS = "Alias" +PLACEHOLDER_NAME = _("Name") +PLACEHOLDER_EMAIL = _("Email") +PLACEHOLDER_CITY = _("City") +PLACEHOLDER_HOMEPAGE = _("Homepage") +PLACEHOLDER_SOCIAL_URL = _("Social network URL") +PLACEHOLDER_ALIAS = _("Alias") + +# Control prompts +PROMPT_SOCIAL_NETWORK = _("Social Network") # Section headers -SECTION_SOCIAL = "Social Networks" -SECTION_ALIASES = "Aliases" -SECTION_WHO = "Who are you and what do you do?" -SECTION_PYTHON = "How do you program in Python?" -SECTION_CONTRIB = "Do you have any contributions to the Python community?" -SECTION_AVAIL = "Are you available for mentoring, consulting, talks?" +SECTION_SOCIAL = _("Social Networks") +SECTION_ALIASES = _("Aliases") +SECTION_WHO = _("Who are you and what do you do?") +SECTION_PYTHON = _("How do you program in Python?") +SECTION_CONTRIB = _("Do you have any contributions to the Python community?") +SECTION_AVAIL = _("Are you available for mentoring, consulting, talks?") # Messages -MESSAGE_PROMPT_FOR_GITHUB_TOKEN = ( +MESSAGE_PROMPT_FOR_GITHUB_TOKEN = _( "Please enter your GitHub personal access token: " ) -MESSAGE_EXIT = "See you next time!" -MESSAGE_QUIT = "Exiting the application." -MESSAGE_FILE_READ_ERROR = "Error reading file {filename}: {error}" -MESSAGE_UNAUTHORIZED = "Unauthorized access. Please check your access token." -MESSAGE_REPO_NOT_FOUND = ( +MESSAGE_EXIT = _("See you next time!") +MESSAGE_FILE_READ_ERROR = _("Error reading file {filename}: {error}") +MESSAGE_UNAUTHORIZED = _( + "Unauthorized access. Please check your access token." +) +MESSAGE_REPO_NOT_FOUND = _( "Repository not found. Please check your access token." ) -MESSAGE_FILE_EDITED_PR = ( +MESSAGE_FILE_EDITED_PR = _( "File {name_file} edited, commit and changes sent to existing PR." ) -MESSAGE_FILE_SAVED_PR = "File {name_file} saved, commit and PR ready." -MESSAGE_CREATE_ENTRY = ( +MESSAGE_FILE_SAVED_PR = _("File {name_file} saved, commit and PR ready.") +MESSAGE_CREATE_ENTRY = _( "Creating a new entry to `blog/members` for {name} (alias: {first_alias})." ) -MESSAGE_CHANGE_ENTRY = ( +MESSAGE_CHANGE_ENTRY = _( "Changing an entry to `blog/members` for {name} (alias: {first_alias})." ) -MESSAGE_LOAD_FILE_ERROR = "Error reading file {filename}: {error}" +MESSAGE_LOAD_FILE_ERROR = _("Error reading file {filename}: {error}") # build_md_content markdown dictionary (English keys, Spanish values for now) MD_CONTENT = { diff --git a/translations/es/LC_MESSAGES/messages.mo b/translations/es/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..f59f6b2 Binary files /dev/null and b/translations/es/LC_MESSAGES/messages.mo differ diff --git a/translations/fr/LC_MESSAGES/messages.mo b/translations/fr/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..b1058b3 Binary files /dev/null and b/translations/fr/LC_MESSAGES/messages.mo differ diff --git a/translations/it/LC_MESSAGES/messages.mo b/translations/it/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..50eb607 Binary files /dev/null and b/translations/it/LC_MESSAGES/messages.mo differ diff --git a/uv.lock b/uv.lock index 29e54eb..3373552 100644 --- a/uv.lock +++ b/uv.lock @@ -79,6 +79,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, +] + [[package]] name = "black" version = "25.1.0" @@ -208,6 +230,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957, upload-time = "2025-03-02T00:01:06.987Z" }, ] +[[package]] +name = "deep-translator" +version = "1.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/03/8fa7635c729a01de71151894cdf002ad6d245bfd6d1a731da864cf534dcf/deep_translator-1.11.4.tar.gz", hash = "sha256:801260c69231138707ea88a0955e484db7d40e210c9e0ae0f77372ffda5f4bf5", size = 36043, upload-time = "2023-06-28T19:55:23.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3f/61a8ef73236dbea83a1a063a8af2f8e1e41a0df64f122233938391d0f175/deep_translator-1.11.4-py3-none-any.whl", hash = "sha256:d635df037e23fa35d12fd42dab72a0b55c9dd19e6292009ee7207e3f30b9e60a", size = 42285, upload-time = "2023-06-28T19:55:20.928Z" }, +] + [[package]] name = "deprecated" version = "1.2.18" @@ -225,6 +260,7 @@ name = "edit-python-pe" version = "0.2.1" source = { editable = "." } dependencies = [ + { name = "babel" }, { name = "platformdirs" }, { name = "pygit2" }, { name = "pygithub" }, @@ -235,26 +271,31 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "black" }, + { name = "deep-translator" }, { name = "isort" }, + { name = "polib" }, { name = "pytest" }, { name = "textual-dev" }, ] [package.metadata] requires-dist = [ - { name = "platformdirs", specifier = ">=4.3.7" }, + { name = "babel", specifier = "==2.17.0" }, + { name = "platformdirs", specifier = "==4.3.7" }, { name = "pygit2", specifier = "==1.17.0" }, { name = "pygithub", specifier = "==2.6.1" }, - { name = "pyyaml", specifier = ">=6.0" }, + { name = "pyyaml", specifier = "==6.0.2" }, { name = "textual", specifier = "==3.1.0" }, ] [package.metadata.requires-dev] dev = [ { name = "black", specifier = ">=25.1.0" }, + { name = "deep-translator", specifier = ">=1.11.4" }, { name = "isort", specifier = ">=6.0.1" }, + { name = "polib", specifier = ">=1.2.0" }, { name = "pytest", specifier = ">=8.4.1" }, - { name = "textual-dev", specifier = ">=1.7.0" }, + { name = "textual-dev", specifier = "==1.7.0" }, ] [[package]] @@ -528,6 +569,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "polib" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/9a/79b1067d27e38ddf84fe7da6ec516f1743f31f752c6122193e7bce38bdbf/polib-1.2.0.tar.gz", hash = "sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b", size = 161658, upload-time = "2023-02-23T17:53:56.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/99/45bb1f9926efe370c6dbe324741c749658e44cb060124f28dad201202274/polib-1.2.0-py2.py3-none-any.whl", hash = "sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d", size = 20634, upload-time = "2023-02-23T17:53:59.919Z" }, +] + [[package]] name = "propcache" version = "0.3.2" @@ -717,6 +767,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, ] +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + [[package]] name = "textual" version = "3.1.0"