diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6625a8b5..81df6d9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,9 +86,15 @@ jobs: with: enable-cache: true + - name: Install gettext (for compilemessages) + run: sudo apt-get update && sudo apt-get install -y gettext + - name: Install dependencies run: uv sync --dev + - name: Compile translation catalogs + run: uv run python manage.py compilemessages + - name: Run tests run: uv run pytest diff --git a/Containerfile b/Containerfile index a9df83cd..062060bc 100644 --- a/Containerfile +++ b/Containerfile @@ -25,6 +25,16 @@ COPY . . ENV PATH="/app/.venv/bin:$PATH" +# Compile translation catalogs into the image. Uses django-admin (not +# manage.py) so it doesn't require settings/env vars (DB, SECRET_KEY, etc.) +# to be configured at build time. gettext (msgfmt) is only needed here, so +# it's removed again afterward to keep the runtime image slim. +RUN apt-get update \ + && apt-get install -y --no-install-recommends gettext \ + && django-admin compilemessages --ignore=".venv/*" \ + && apt-get purge -y --auto-remove gettext \ + && rm -rf /var/lib/apt/lists/* + EXPOSE 8000 # Workers: 2 * CPU_count + 1 is the recommended starting point. diff --git a/comicsdb/forms/issue.py b/comicsdb/forms/issue.py index d0bc5eea..487a7e21 100644 --- a/comicsdb/forms/issue.py +++ b/comicsdb/forms/issue.py @@ -5,6 +5,7 @@ ModelForm, ValidationError, ) +from django.utils.translation import gettext_lazy as _ from isbnlib import canonical, is_isbn10, is_isbn13 from comicsdb.autocomplete import ( @@ -46,7 +47,7 @@ class IssueForm(ModelForm): widget=SafeAutocompleteWidget( ac_class=SeriesAutocomplete, attrs={ - "placeholder": "Autocomplete...", + "placeholder": _("Autocomplete..."), }, ), ) @@ -94,16 +95,16 @@ class Meta: "image": ClearableFileInput(), } help_texts = { - "alt_number": "Primarily used for legacy numbering for DC and Marvel comics.", - "name": "Separate multiple story titles by a semicolon", - "title": "Only used with Collected Editions like a Trade Paperback.", - "price": "USD for US publishers, GBP for UK publishers", - "reprints": "Search using 'Series Name (Year) #Number' format.", - "foc_date": "This date should be earlier than the store date", + "alt_number": _("Primarily used for legacy numbering for DC and Marvel comics."), + "name": _("Separate multiple story titles by a semicolon"), + "title": _("Only used with Collected Editions like a Trade Paperback."), + "price": _("USD for US publishers, GBP for UK publishers"), + "reprints": _("Search using 'Series Name (Year) #Number' format."), + "foc_date": _("This date should be earlier than the store date"), } labels = { - "name": "Story Title", - "title": "Collection Title", + "name": _("Story Title"), + "title": _("Collection Title"), } def __init__(self, *args, **kwargs): @@ -117,7 +118,7 @@ def __init__(self, *args, **kwargs): def _validate_date(self, field: str): form_date = self.cleaned_data[field] if form_date is not None and form_date.year < MINIMUM_YEAR: - raise ValidationError("Date has a non-valid year.") + raise ValidationError(_("Date has a non-valid year.")) return form_date def clean_store_date(self): @@ -132,7 +133,7 @@ def clean_foc_date(self): def clean_sku(self): sku = self.cleaned_data["sku"] if sku and not sku.isalnum(): - raise ValidationError("SKU must be alphanumeric. No spaces or hyphens allowed.") + raise ValidationError(_("SKU must be alphanumeric. No spaces or hyphens allowed.")) return sku def clean_isbn(self): @@ -141,13 +142,13 @@ def clean_isbn(self): isbn = canonical(data) if is_isbn10(isbn) or is_isbn13(isbn): return isbn - raise ValidationError("ISBN is not a valid ISBN-10 or ISBN-13.") + raise ValidationError(_("ISBN is not a valid ISBN-10 or ISBN-13.")) return isbn def clean_upc(self): upc = self.cleaned_data["upc"] if upc and not upc.isdigit(): - raise ValidationError("UPC must be numeric. No spaces or hyphens allowed.") + raise ValidationError(_("UPC must be numeric. No spaces or hyphens allowed.")) return upc def clean_title(self): @@ -155,7 +156,7 @@ def clean_title(self): if collection_title: series: Series = self.cleaned_data["series"] if not series.collection: - raise ValidationError("Collection Title field is not allowed for this series..") + raise ValidationError(_("Collection Title field is not allowed for this series..")) return collection_title def clean_arcs(self): @@ -163,5 +164,5 @@ def clean_arcs(self): if arcs: series: Series = self.cleaned_data["series"] if series.series_type.id in self.collections: - raise ValidationError("Arcs cannot be added to Trade Paperbacks.") + raise ValidationError(_("Arcs cannot be added to Trade Paperbacks.")) return arcs diff --git a/comicsdb/models/issue.py b/comicsdb/models/issue.py index a3091898..5ed56b91 100644 --- a/comicsdb/models/issue.py +++ b/comicsdb/models/issue.py @@ -12,6 +12,7 @@ from django.db.models.signals import pre_save from django.urls import reverse from django.utils.text import slugify +from django.utils.translation import gettext_lazy as _ from djmoney.models.fields import MoneyField from PIL import Image from simple_history.models import HistoricalRecords @@ -53,21 +54,21 @@ def get_queryset(self): class Issue(CommonInfo): series = models.ForeignKey(Series, on_delete=models.CASCADE, related_name="issues") - name = ArrayField(models.CharField("Story Title", max_length=150), blank=True, default=list) - title = models.CharField("Collection Title", max_length=255, blank=True) + name = ArrayField(models.CharField(_("Story Title"), max_length=150), blank=True, default=list) + title = models.CharField(_("Collection Title"), max_length=255, blank=True) number = models.CharField(max_length=25) - alt_number = models.CharField("Alternative Number", max_length=25, blank=True) - cover_date = models.DateField("Cover Date") - store_date = models.DateField("In Store Date", null=True, blank=True) - foc_date = models.DateField("Final Order Cutoff Date", null=True, blank=True) - price = MoneyField("Cover Price", max_digits=5, decimal_places=2, blank=True, null=True) + alt_number = models.CharField(_("Alternative Number"), max_length=25, blank=True) + cover_date = models.DateField(_("Cover Date")) + store_date = models.DateField(_("In Store Date"), null=True, blank=True) + foc_date = models.DateField(_("Final Order Cutoff Date"), null=True, blank=True) + price = MoneyField(_("Cover Price"), max_digits=5, decimal_places=2, blank=True, null=True) rating = models.ForeignKey(Rating, default=1, on_delete=models.SET_DEFAULT) - sku = models.CharField("Distributor SKU", max_length=12, blank=True) - isbn = models.CharField("ISBN", max_length=13, blank=True) - upc = models.CharField("UPC Code", max_length=20, blank=True) - page = models.PositiveSmallIntegerField("Page Count", null=True, blank=True) - image = ImageField("Cover", upload_to="issue/%Y/%m/%d/", blank=True) - cover_hash = models.CharField("Cover Hash", max_length=16, blank=True) + sku = models.CharField(_("Distributor SKU"), max_length=12, blank=True) + isbn = models.CharField(_("ISBN"), max_length=13, blank=True) + upc = models.CharField(_("UPC Code"), max_length=20, blank=True) + page = models.PositiveSmallIntegerField(_("Page Count"), null=True, blank=True) + image = ImageField(_("Cover"), upload_to="issue/%Y/%m/%d/", blank=True) + cover_hash = models.CharField(_("Cover Hash"), max_length=16, blank=True) arcs = models.ManyToManyField(Arc, blank=True, related_name="issues") creators = models.ManyToManyField(Creator, through="Credits", blank=True, related_name="issues") characters = models.ManyToManyField(Character, blank=True, related_name="issues") diff --git a/comicsdb/views/issue.py b/comicsdb/views/issue.py index 42521fe1..5fa7ff73 100644 --- a/comicsdb/views/issue.py +++ b/comicsdb/views/issue.py @@ -11,6 +11,7 @@ from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext as _ from django.views import View from django.views.generic import DetailView, ListView from django.views.generic.edit import CreateView, DeleteView, UpdateView @@ -447,7 +448,8 @@ def post(self, request, slug): if issue.series.publisher.name in excluded_publishers: messages.error( request, - f"Credit duplication is not allowed for {issue.series.publisher.name} issues.", + _("Credit duplication is not allowed for %(publisher)s issues.") + % {"publisher": issue.series.publisher.name}, ) return HttpResponseRedirect(reverse("issue:detail", args=[slug])) @@ -455,8 +457,11 @@ def post(self, request, slug): if issue.credits_set.exists(): messages.info( request, - f"{issue} already has credits assigned. " - "Duplicate operation cancelled to preserve existing data.", + _( + "%(issue)s already has credits assigned. " + "Duplicate operation cancelled to preserve existing data." + ) + % {"issue": issue}, ) return HttpResponseRedirect(reverse("issue:detail", args=[slug])) @@ -464,14 +469,19 @@ def post(self, request, slug): try: previous_issue = issue.get_previous_by_cover_date(series=issue.series) except ObjectDoesNotExist: - messages.warning(request, f"No previous issue found for {issue}.") + messages.warning( + request, _("No previous issue found for %(issue)s.") % {"issue": issue} + ) return HttpResponseRedirect(reverse("issue:detail", args=[slug])) # Get all credits from the previous issue previous_credits = Credits.objects.filter(issue=previous_issue).prefetch_related("role") if not previous_credits.exists(): - messages.info(request, f"No credits found in {previous_issue} to duplicate.") + messages.info( + request, + _("No credits found in %(issue)s to duplicate.") % {"issue": previous_issue}, + ) return HttpResponseRedirect(reverse("issue:detail", args=[slug])) # Get the Cover role to check for variant cover credits @@ -521,20 +531,35 @@ def post(self, request, slug): # Provide appropriate feedback based on what was duplicated if credits_count > 0: - message = f"Successfully duplicated {credits_count} credit(s) from {previous_issue}." if skipped_count > 0: - message += f" Skipped {skipped_count} variant cover credit(s)." + message = _( + "Successfully duplicated %(credits)s credit(s) from %(issue)s." + " Skipped %(skipped)s variant cover credit(s)." + ) % { + "credits": credits_count, + "issue": previous_issue, + "skipped": skipped_count, + } + else: + message = _("Successfully duplicated %(credits)s credit(s) from %(issue)s.") % { + "credits": credits_count, + "issue": previous_issue, + } messages.success(request, message) elif skipped_count > 0: messages.info( request, - ( - f"No credits were duplicated. Skipped {skipped_count} variant cover credit(s)" - f" from {previous_issue}." - ), + _( + "No credits were duplicated. Skipped %(skipped)s variant cover credit(s)" + " from %(issue)s." + ) + % {"skipped": skipped_count, "issue": previous_issue}, ) else: - messages.info(request, f"No credits found in {previous_issue} to duplicate.") + messages.info( + request, + _("No credits found in %(issue)s to duplicate.") % {"issue": previous_issue}, + ) return HttpResponseRedirect(reverse("issue:detail", args=[slug])) @@ -545,7 +570,7 @@ class IssueReprintSyncView(LoginRequiredMixin, View): to the current issue. """ - def post(self, request, slug): # noqa: PLR0912 + def post(self, request, slug): # noqa: PLR0912, PLR0915 """ Add characters, teams, and story titles from all reprinted issues to the current issue. Only works for Trade Paperback, Omnibus, and Hardcover series types. @@ -572,17 +597,22 @@ def post(self, request, slug): # noqa: PLR0912 # Check if series type is Trade Paperback or Omnibus allowed_types = ["Trade Paperback", "Omnibus", "Hardcover"] if issue.series.series_type.name not in allowed_types: + type_list = f"{', '.join(allowed_types[:-1])} and {allowed_types[-1]}" messages.error( request, - f"This function only works for {', '.join(allowed_types[:-1])} " - f"and {allowed_types[-1]} series types. " - f"This issue is of type '{issue.series.series_type.name}'.", + _( + "This function only works for %(types)s series types. " + "This issue is of type '%(current_type)s'." + ) + % {"types": type_list, "current_type": issue.series.series_type.name}, ) return HttpResponseRedirect(reverse("issue:detail", args=[slug])) # Check if there are any reprints if not issue.reprints.exists(): - messages.warning(request, f"No reprinted issues found for {issue}.") + messages.warning( + request, _("No reprinted issues found for %(issue)s.") % {"issue": issue} + ) return HttpResponseRedirect(reverse("issue:detail", args=[slug])) # Check if issue already has characters, teams, or stories @@ -590,8 +620,11 @@ def post(self, request, slug): # noqa: PLR0912 if issue.characters.exists() or issue.teams.exists() or has_stories: messages.info( request, - f"{issue} already has characters, teams, or stories assigned. " - "Sync operation cancelled to preserve existing data.", + _( + "%(issue)s already has characters, teams, or stories assigned. " + "Sync operation cancelled to preserve existing data." + ) + % {"issue": issue}, ) return HttpResponseRedirect(reverse("issue:detail", args=[slug])) @@ -619,16 +652,18 @@ def post(self, request, slug): # noqa: PLR0912 # Inform user if any reprints were skipped if skipped_reprints: + reprint_names = ", ".join(skipped_reprints[:3]) + if len(skipped_reprints) > 3: # noqa: PLR2004 + reprint_names += "..." messages.warning( request, - f"Skipped {len(skipped_reprints)} reprinted issue(s) with multiple story titles: " - f"{', '.join(skipped_reprints[:3])}" - f"{'...' if len(skipped_reprints) > 3 else ''}", # noqa: PLR2004 + _("Skipped %(count)s reprinted issue(s) with multiple story titles: %(names)s") + % {"count": len(skipped_reprints), "names": reprint_names}, ) # Check if we found any characters or teams to add if not characters_to_add and not teams_to_add: - messages.info(request, "No characters or teams found in the reprinted issues.") + messages.info(request, _("No characters or teams found in the reprinted issues.")) return HttpResponseRedirect(reverse("issue:detail", args=[slug])) # Add all characters, teams, and stories from reprints @@ -668,18 +703,23 @@ def post(self, request, slug): # noqa: PLR0912 # Provide feedback message_parts = [] if characters_to_add: - message_parts.append(f"{len(characters_to_add)} character(s)") + message_parts.append(_("%(count)s character(s)") % {"count": len(characters_to_add)}) if teams_to_add: - message_parts.append(f"{len(teams_to_add)} team(s)") + message_parts.append(_("%(count)s team(s)") % {"count": len(teams_to_add)}) if stories_to_add: - message_parts.append(f"{len(stories_to_add)} story title(s)") + message_parts.append(_("%(count)s story title(s)") % {"count": len(stories_to_add)}) + + if len(message_parts) > 1: + parts_text = _("%(first)s and %(last)s") % { + "first": ", ".join(message_parts[:-1]), + "last": message_parts[-1], + } + else: + parts_text = message_parts[0] messages.success( request, - f"Successfully added {', '.join(message_parts[:-1])} and " - f"{message_parts[-1]} from reprinted issues." - if len(message_parts) > 1 - else f"Successfully added {message_parts[0]} from reprinted issues.", + _("Successfully added %(parts)s from reprinted issues.") % {"parts": parts_text}, ) return HttpResponseRedirect(reverse("issue:detail", args=[slug])) diff --git a/locale/it/LC_MESSAGES/django.po b/locale/it/LC_MESSAGES/django.po new file mode 100644 index 00000000..ae1f444d --- /dev/null +++ b/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,499 @@ +# Italian translation catalog for Metron. +# Copyright (C) 2026 Metron Project +# This file is distributed under the same license as the Metron package. +# +msgid "" +msgstr "" +"Project-Id-Version: metron\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-08-13 11:23-0400\n" +"PO-Revision-Date: 2026-08-13 11:23-0400\n" +"Last-Translator: Metron Project\n" +"Language-Team: Italian\n" +"Language: it\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" + +#: comicsdb/admin/issue.py:119 comicsdb/admin/issue.py:148 +#: comicsdb/admin/issue.py:170 comicsdb/admin/series.py:61 +#, python-format +msgid "%d issue was updated." +msgid_plural "%d issues were updated." +msgstr[0] "%d albo è stato aggiornato." +msgstr[1] "%d albi sono stati aggiornati." + +#: comicsdb/admin/issue.py:212 +#, python-format +msgid "%d Trade Paperback was updated." +msgid_plural "%d Trade Paperbacks were updated." +msgstr[0] "%d Trade Paperback è stato aggiornato." +msgstr[1] "%d Trade Paperback sono stati aggiornati." + +#: comicsdb/admin/issue.py:233 +#, python-format +msgid "%d issue was updated" +msgid_plural "%d issues were updated" +msgstr[0] "%d albo è stato aggiornato" +msgstr[1] "%d albi sono stati aggiornati" + +#: comicsdb/forms/issue.py:50 +msgid "Autocomplete..." +msgstr "Completamento automatico..." + +#: comicsdb/forms/issue.py:98 +msgid "Primarily used for legacy numbering for DC and Marvel comics." +msgstr "Utilizzato principalmente per la numerazione storica dei fumetti DC e Marvel." + +#: comicsdb/forms/issue.py:99 +msgid "Separate multiple story titles by a semicolon" +msgstr "Separa più titoli di storie con un punto e virgola" + +#: comicsdb/forms/issue.py:100 +msgid "Only used with Collected Editions like a Trade Paperback." +msgstr "Utilizzato solo con edizioni raccolte come un Trade Paperback." + +#: comicsdb/forms/issue.py:101 +msgid "USD for US publishers, GBP for UK publishers" +msgstr "USD per editori statunitensi, GBP per editori britannici" + +#: comicsdb/forms/issue.py:102 +msgid "Search using 'Series Name (Year) #Number' format." +msgstr "Cerca usando il formato 'Nome Serie (Anno) #Numero'." + +#: comicsdb/forms/issue.py:103 +msgid "This date should be earlier than the store date" +msgstr "Questa data dovrebbe essere precedente alla data di uscita in negozio" + +#: comicsdb/forms/issue.py:106 comicsdb/models/issue.py:57 +msgid "Story Title" +msgstr "Titolo della Storia" + +#: comicsdb/forms/issue.py:107 comicsdb/models/issue.py:58 +msgid "Collection Title" +msgstr "Titolo della Raccolta" + +#: comicsdb/forms/issue.py:121 +msgid "Date has a non-valid year." +msgstr "La data ha un anno non valido." + +#: comicsdb/forms/issue.py:136 +msgid "SKU must be alphanumeric. No spaces or hyphens allowed." +msgstr "Lo SKU deve essere alfanumerico. Non sono ammessi spazi o trattini." + +#: comicsdb/forms/issue.py:145 +msgid "ISBN is not a valid ISBN-10 or ISBN-13." +msgstr "L'ISBN non è un ISBN-10 o ISBN-13 valido." + +#: comicsdb/forms/issue.py:151 +msgid "UPC must be numeric. No spaces or hyphens allowed." +msgstr "L'UPC deve essere numerico. Non sono ammessi spazi o trattini." + +#: comicsdb/forms/issue.py:159 +msgid "Collection Title field is not allowed for this series.." +msgstr "Il campo Titolo della Raccolta non è consentito per questa serie." + +#: comicsdb/forms/issue.py:167 +msgid "Arcs cannot be added to Trade Paperbacks." +msgstr "Gli archi narrativi non possono essere aggiunti ai Trade Paperback." + +#: comicsdb/models/issue.py:60 +msgid "Alternative Number" +msgstr "Numero Alternativo" + +#: comicsdb/models/issue.py:61 +msgid "Cover Date" +msgstr "Data di Copertina" + +#: comicsdb/models/issue.py:62 +msgid "In Store Date" +msgstr "Data di Uscita in Negozio" + +#: comicsdb/models/issue.py:63 +msgid "Final Order Cutoff Date" +msgstr "Data Limite per l'Ordine Finale" + +#: comicsdb/models/issue.py:64 +msgid "Cover Price" +msgstr "Prezzo di Copertina" + +#: comicsdb/models/issue.py:66 +msgid "Distributor SKU" +msgstr "SKU del Distributore" + +#: comicsdb/models/issue.py:67 +msgid "ISBN" +msgstr "ISBN" + +#: comicsdb/models/issue.py:68 +msgid "UPC Code" +msgstr "Codice UPC" + +#: comicsdb/models/issue.py:69 +msgid "Page Count" +msgstr "Numero di Pagine" + +#: comicsdb/models/issue.py:70 +msgid "Cover" +msgstr "Copertina" + +#: comicsdb/models/issue.py:71 +msgid "Cover Hash" +msgstr "Hash della Copertina" + +#: comicsdb/views/issue.py:451 +#, python-format +msgid "Credit duplication is not allowed for %(publisher)s issues." +msgstr "La duplicazione dei crediti non è consentita per gli albi di %(publisher)s." + +#: comicsdb/views/issue.py:461 +#, python-format +msgid "" +"%(issue)s already has credits assigned. Duplicate operation cancelled to " +"preserve existing data." +msgstr "" +"%(issue)s ha già dei crediti assegnati. Operazione di duplicazione " +"annullata per preservare i dati esistenti." + +#: comicsdb/views/issue.py:473 +#, python-format +msgid "No previous issue found for %(issue)s." +msgstr "Nessun albo precedente trovato per %(issue)s." + +#: comicsdb/views/issue.py:483 comicsdb/views/issue.py:561 +#, python-format +msgid "No credits found in %(issue)s to duplicate." +msgstr "Nessun credito trovato in %(issue)s da duplicare." + +#: comicsdb/views/issue.py:536 +#, python-format +msgid "" +"Successfully duplicated %(credits)s credit(s) from %(issue)s. Skipped " +"%(skipped)s variant cover credit(s)." +msgstr "" +"Duplicati con successo %(credits)s credito/i da %(issue)s. Saltati " +"%(skipped)s credito/i di copertina variante." + +#: comicsdb/views/issue.py:544 +#, python-format +msgid "Successfully duplicated %(credits)s credit(s) from %(issue)s." +msgstr "Duplicati con successo %(credits)s credito/i da %(issue)s." + +#: comicsdb/views/issue.py:553 +#, python-format +msgid "" +"No credits were duplicated. Skipped %(skipped)s variant cover credit(s) from " +"%(issue)s." +msgstr "" +"Nessun credito è stato duplicato. Saltati %(skipped)s credito/i di " +"copertina variante da %(issue)s." + +#: comicsdb/views/issue.py:604 +#, python-format +msgid "" +"This function only works for %(types)s series types. This issue is of type " +"'%(current_type)s'." +msgstr "" +"Questa funzione funziona solo per i tipi di serie %(types)s. Questo albo è " +"di tipo '%(current_type)s'." + +#: comicsdb/views/issue.py:614 +#, python-format +msgid "No reprinted issues found for %(issue)s." +msgstr "Nessun albo ristampato trovato per %(issue)s." + +#: comicsdb/views/issue.py:624 +#, python-format +msgid "" +"%(issue)s already has characters, teams, or stories assigned. Sync operation " +"cancelled to preserve existing data." +msgstr "" +"%(issue)s ha già personaggi, squadre o storie assegnate. Operazione di " +"sincronizzazione annullata per preservare i dati esistenti." + +#: comicsdb/views/issue.py:660 +#, python-format +msgid "" +"Skipped %(count)s reprinted issue(s) with multiple story titles: %(names)s" +msgstr "" +"Saltati %(count)s albo/i ristampato/i con più titoli di storie: %(names)s" + +#: comicsdb/views/issue.py:666 +msgid "No characters or teams found in the reprinted issues." +msgstr "Nessun personaggio o squadra trovati negli albi ristampati." + +#: comicsdb/views/issue.py:706 +#, python-format +msgid "%(count)s character(s)" +msgstr "%(count)s personaggio/i" + +#: comicsdb/views/issue.py:708 +#, python-format +msgid "%(count)s team(s)" +msgstr "%(count)s squadra/e" + +#: comicsdb/views/issue.py:710 +#, python-format +msgid "%(count)s story title(s)" +msgstr "%(count)s titolo/i di storia" + +#: comicsdb/views/issue.py:713 +#, python-format +msgid "%(first)s and %(last)s" +msgstr "%(first)s e %(last)s" + +#: comicsdb/views/issue.py:722 +#, python-format +msgid "Successfully added %(parts)s from reprinted issues." +msgstr "Aggiunti con successo %(parts)s dagli albi ristampati." + +#: templates/base.html:9 +msgid "Metron Comic Book Database" +msgstr "Database Fumetti Metron" + +#: templates/base.html:13 +msgid "" +"A community-based comic book database with comprehensive information about " +"comics, creators, characters, and more." +msgstr "" +"Un database di fumetti gestito dalla community, con informazioni complete su " +"fumetti, autori, personaggi e altro ancora." + +#: templates/base.html:17 +msgid "comics, comic books, database, Marvel, DC, creators, characters" +msgstr "fumetti, albi, database, Marvel, DC, autori, personaggi" + +#: templates/partials/footer.html:10 +msgid "Contact Us" +msgstr "Contattaci" + +#: templates/partials/footer.html:11 +msgid "Contact links" +msgstr "Link di contatto" + +#: templates/partials/footer.html:16 +msgid "Email site administrator" +msgstr "Invia un'email all'amministratore del sito" + +#: templates/partials/footer.html:20 +msgid "Email Site Admin" +msgstr "Email all'Amministratore" + +#: templates/partials/footer.html:28 +msgid "Join our Matrix chat" +msgstr "Unisciti alla nostra chat Matrix" + +#: templates/partials/footer.html:32 +msgid "Matrix Chat" +msgstr "Chat Matrix" + +#: templates/partials/footer.html:40 +msgid "Join GitHub discussions" +msgstr "Partecipa alle discussioni su GitHub" + +#: templates/partials/footer.html:44 +msgid "GitHub Discussions" +msgstr "Discussioni GitHub" + +#: templates/partials/footer.html:55 +msgid "Built with" +msgstr "Realizzato con" + +#: templates/partials/footer.html:62 +msgctxt "connecting two project names" +msgid "and" +msgstr "e" + +#: templates/partials/footer.html:73 +msgid "This work is licensed under a" +msgstr "Quest'opera è distribuita con licenza" + +#: templates/partials/footer.html:78 +msgid "Creative Commons Attribution-ShareAlike 4.0 International License" +msgstr "Creative Commons Attribuzione-Condividi allo stesso modo 4.0 Internazionale" + +#: templates/partials/footer.html:79 +msgid ", except where noted otherwise." +msgstr ", salvo dove diversamente indicato." + +#: templates/partials/footer.html:84 templates/partials/footer.html:86 +msgid "Creative Commons License" +msgstr "Licenza Creative Commons" + +#: templates/partials/footer.html:95 +#, python-format +msgid "© %(current_year)s Metron Project. All rights reserved." +msgstr "© %(current_year)s Metron Project. Tutti i diritti riservati." + +#: templates/partials/messages.html:13 +msgid "close notification" +msgstr "chiudi notifica" + +#: templates/partials/navbar.html:5 +msgid "main navigation" +msgstr "navigazione principale" + +#: templates/partials/navbar.html:10 +msgid "Metron home" +msgstr "Home di Metron" + +#: templates/partials/navbar.html:12 +msgid "Metron logo" +msgstr "Logo Metron" + +#: templates/partials/navbar.html:18 +msgid "menu" +msgstr "menu" + +#: templates/partials/navbar.html:32 +msgid "Releases" +msgstr "Uscite" + +#: templates/partials/navbar.html:38 +msgid "This Week" +msgstr "Questa Settimana" + +#: templates/partials/navbar.html:44 +msgid "Next Week" +msgstr "Prossima Settimana" + +#: templates/partials/navbar.html:50 +msgid "Future" +msgstr "Future" + +#: templates/partials/navbar.html:56 +msgid "Browse" +msgstr "Esplora" + +#: templates/partials/navbar.html:62 +msgid "Publishers" +msgstr "Editori" + +#: templates/partials/navbar.html:68 +msgid "Imprints" +msgstr "Etichette" + +#: templates/partials/navbar.html:74 +msgid "Series" +msgstr "Serie" + +#: templates/partials/navbar.html:80 +msgid "Issues" +msgstr "Albi" + +#: templates/partials/navbar.html:87 +msgid "Creators" +msgstr "Autori" + +#: templates/partials/navbar.html:93 +msgid "Characters" +msgstr "Personaggi" + +#: templates/partials/navbar.html:99 +msgid "Teams" +msgstr "Squadre" + +#: templates/partials/navbar.html:106 +msgid "Story Arcs" +msgstr "Archi Narrativi" + +#: templates/partials/navbar.html:112 +msgid "Universes" +msgstr "Universi" + +#: templates/partials/navbar.html:119 +msgid "Reading Lists" +msgstr "Liste di Lettura" + +#: templates/partials/navbar.html:126 +msgid "My Library" +msgstr "La Mia Libreria" + +#: templates/partials/navbar.html:132 +msgid "My Reading Lists" +msgstr "Le Mie Liste di Lettura" + +#: templates/partials/navbar.html:138 +msgid "My Collection" +msgstr "La Mia Collezione" + +#: templates/partials/navbar.html:144 +msgid "My Pull List" +msgstr "La Mia Pull List" + +#: templates/partials/navbar.html:150 +msgid "My Wish List" +msgstr "La Mia Lista dei Desideri" + +#: templates/partials/navbar.html:157 +msgid "More" +msgstr "Altro" + +#: templates/partials/navbar.html:163 +msgid "Wiki" +msgstr "Wiki" + +#: templates/partials/navbar.html:170 +msgid "API Docs" +msgstr "Documentazione API" + +#: templates/partials/navbar.html:177 +msgid "Polls" +msgstr "Sondaggi" + +#: templates/partials/navbar.html:183 +msgid "Statistics" +msgstr "Statistiche" + +#: templates/partials/navbar.html:189 +msgid "Users" +msgstr "Utenti" + +#: templates/partials/navbar.html:198 +msgid "Blog" +msgstr "Blog" + +#: templates/partials/navbar.html:211 +msgid "Report an Issue" +msgstr "Segnala un Problema" + +#: templates/partials/navbar.html:226 +msgid "Metron on GitHub" +msgstr "Metron su GitHub" + +#: templates/partials/navbar.html:235 +msgid "Metron on Mastodon" +msgstr "Metron su Mastodon" + +#: templates/partials/navbar.html:244 +msgid "View your account" +msgstr "Visualizza il tuo account" + +#: templates/partials/navbar.html:248 +msgid "Account" +msgstr "Account" + +#: templates/partials/navbar.html:258 +msgid "Log out of your account" +msgstr "Esci dal tuo account" + +#: templates/partials/navbar.html:262 +msgid "Log out" +msgstr "Esci" + +#: templates/partials/navbar.html:268 +msgid "Create a new account" +msgstr "Crea un nuovo account" + +#: templates/partials/navbar.html:269 +msgid "Sign up" +msgstr "Registrati" + +#: templates/partials/navbar.html:273 +msgid "Log in to your account" +msgstr "Accedi al tuo account" + +#: templates/partials/navbar.html:273 +msgid "Log in" +msgstr "Accedi" diff --git a/metron/settings.py b/metron/settings.py index cea311c0..6cfba5c4 100644 --- a/metron/settings.py +++ b/metron/settings.py @@ -87,6 +87,7 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -112,6 +113,7 @@ "django.contrib.messages.context_processors.messages", "comicsdb.context_processors.announcement_context_processor", "sekizai.context_processors.sekizai", + "django.template.context_processors.i18n", ], # Bulma templatetags for project. "libraries": { @@ -274,6 +276,15 @@ LANGUAGE_CODE = "en-us" +LANGUAGES = [ + ("en", "English"), + ("it", "Italiano"), +] + +LOCALE_PATHS = [ + BASE_DIR / "locale", +] + TIME_ZONE = "America/New_York" USE_I18N = True diff --git a/metron/urls.py b/metron/urls.py index 5d33efd4..468c3a39 100644 --- a/metron/urls.py +++ b/metron/urls.py @@ -9,7 +9,7 @@ from django.templatetags.static import static from django.urls import include, path from django.views.generic import RedirectView, TemplateView -from django.views.i18n import JavaScriptCatalog +from django.views.i18n import JavaScriptCatalog, set_language from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from api import urls as api_urls @@ -61,6 +61,7 @@ path("imprint/", include(imprint_urls)), path("issue/", include(issue_urls)), path("issue-ratings/", include(issue_ratings_urls)), + path("i18n/setlang/", set_language, name="set_language"), path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), path("publisher/", include(publisher_urls)), path("polls/", include(polls_urls)), diff --git a/templates/base.html b/templates/base.html index 3be10c82..d8b3b81a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,20 +1,20 @@ -{% load static django_htmx %} +{% load static django_htmx i18n %} - + {# SEO Meta Tags #} - {% block title %}Metron Comic Book Database{% endblock %} + {% block title %}{% trans "Metron Comic Book Database" %}{% endblock %} {% block sitedesc %} + content="{% trans 'A community-based comic book database with comprehensive information about comics, creators, characters, and more.' %}"> {% endblock %} {% block sitekeywords %} + content="{% trans 'comics, comic books, database, Marvel, DC, creators, characters' %}"> {% endblock %} {# Social Media Meta Tags #} {% block social_meta %} diff --git a/templates/partials/footer.html b/templates/partials/footer.html index 019883ca..d6f8a138 100644 --- a/templates/partials/footer.html +++ b/templates/partials/footer.html @@ -1,3 +1,4 @@ +{% load i18n %} {% comment %} Site footer with contact information, credits, and license {% endcomment %} @@ -6,17 +7,17 @@
{# Contact Information #}
-

Contact Us

-
diff --git a/templates/partials/language_switcher.html b/templates/partials/language_switcher.html new file mode 100644 index 00000000..13cad8da --- /dev/null +++ b/templates/partials/language_switcher.html @@ -0,0 +1,21 @@ +{% load i18n %} + diff --git a/templates/partials/messages.html b/templates/partials/messages.html index f8c027a8..efb2a0a4 100644 --- a/templates/partials/messages.html +++ b/templates/partials/messages.html @@ -1,4 +1,4 @@ -{% load bulma_tags %} +{% load bulma_tags i18n %} {% comment %} Django messages display component Shows success, error, warning, and info messages @@ -10,7 +10,7 @@ role="alert" {% if message.tags != 'error' and message.tags != 'danger' %}data-auto-dismiss{% endif %}>
diff --git a/templates/partials/navbar.html b/templates/partials/navbar.html index ecfbd6fb..3c292260 100644 --- a/templates/partials/navbar.html +++ b/templates/partials/navbar.html @@ -1,20 +1,21 @@ {% load static %} +{% load i18n %}