fix: improved language selector
CI / test (push) Successful in 1m0s
CI / build (push) Successful in 28s

This commit is contained in:
2026-07-20 12:38:33 +02:00
parent 73109a268b
commit 55186a0891
7 changed files with 127 additions and 86 deletions
+30 -28
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: crochet\n" "Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-20 09:18+0000\n" "POT-Creation-Date: 2026-07-20 10:32+0000\n"
"Language: en\n" "Language: en\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -47,35 +47,33 @@ msgstr "Create pattern"
msgid "¿Seguro que quieres eliminar este patrón? No podrás deshacerlo." msgid "¿Seguro que quieres eliminar este patrón? No podrás deshacerlo."
msgstr "Are you sure you want to delete this pattern? This cannot be undone." msgstr "Are you sure you want to delete this pattern? This cannot be undone."
#: crochet/templates/crochet/account_home.html:34 #: crochet/templates/crochet/account_home.html:37
msgid "Patrón sin título" msgid "Patrón sin título"
msgstr "Untitled pattern" msgstr "Untitled pattern"
#: crochet/templates/crochet/account_home.html:37 #: crochet/templates/crochet/account_home.html:41
#, python-format #, python-format
msgid "Actualizado el %(date)s" msgid "Actualizado el %(date)s"
msgstr "Updated on %(date)s" msgstr "Updated on %(date)s"
#: crochet/templates/crochet/account_home.html:40 #: crochet/templates/crochet/account_home.html:44
#: crochet/templates/crochet/pattern.html:33 #: crochet/templates/crochet/pattern.html:143
msgid "Ver patrón" #: crochet/templates/crochet/pattern.html:189
msgstr "View pattern" msgid "Vista previa"
msgstr "Preview"
#: crochet/templates/crochet/account_home.html:41 #: crochet/templates/crochet/account_home.html:48
msgid "Editar" #: crochet/templates/crochet/pattern.html:38
msgstr "Edit"
#: crochet/templates/crochet/account_home.html:45
msgid "Eliminar" msgid "Eliminar"
msgstr "Delete" msgstr "Delete"
#: crochet/templates/crochet/account_home.html:54 #: crochet/templates/crochet/account_home.html:57
msgid "Todavía no tienes ningún patrón." msgid "Todavía no tienes ningún patrón."
msgstr "You don't have any patterns yet." msgstr "You don't have any patterns yet."
#: crochet/templates/crochet/account_settings.html:4 #: crochet/templates/crochet/account_settings.html:4
#: crochet/templates/crochet/account_settings.html:18 #: crochet/templates/crochet/account_settings.html:18
#: crochet/templates/crochet/base.html:50 #: crochet/templates/crochet/base.html:52
msgid "Ajustes de la cuenta" msgid "Ajustes de la cuenta"
msgstr "Account settings" msgstr "Account settings"
@@ -84,21 +82,21 @@ msgid "Volver a mis patrones"
msgstr "Back to my patterns" msgstr "Back to my patterns"
#: crochet/templates/crochet/base.html:39 #: crochet/templates/crochet/base.html:39
msgid "Idioma" msgid "Cambiar idioma"
msgstr "Language" msgstr "Change language"
#: crochet/templates/crochet/base.html:54 #: crochet/templates/crochet/base.html:56
msgid "Cerrar sesión" msgid "Cerrar sesión"
msgstr "Log out" msgstr "Log out"
#: crochet/templates/crochet/base.html:60 #: crochet/templates/crochet/base.html:62
#: crochet/templates/crochet/home.html:24 #: crochet/templates/crochet/home.html:24
#: crochet/templates/registration/login.html:4 #: crochet/templates/registration/login.html:4
#: crochet/templates/registration/login.html:14 #: crochet/templates/registration/login.html:14
msgid "Iniciar sesión" msgid "Iniciar sesión"
msgstr "Log in" msgstr "Log in"
#: crochet/templates/crochet/base.html:61 #: crochet/templates/crochet/base.html:63
#: crochet/templates/registration/register.html:29 #: crochet/templates/registration/register.html:29
msgid "Registrarme" msgid "Registrarme"
msgstr "Sign up" msgstr "Sign up"
@@ -172,14 +170,14 @@ msgstr "Couldn't save. Please try again."
msgid "Guardar" msgid "Guardar"
msgstr "Save" msgstr "Save"
#: crochet/templates/crochet/pattern.html:33
msgid "Ver patrón"
msgstr "View pattern"
#: crochet/templates/crochet/pattern.html:34 #: crochet/templates/crochet/pattern.html:34
msgid "Exportar a PDF" msgid "Exportar a PDF"
msgstr "Export to PDF" msgstr "Export to PDF"
#: crochet/templates/crochet/pattern.html:38
msgid "Eliminar patrón"
msgstr "Delete pattern"
#: crochet/templates/crochet/pattern.html:45 #: crochet/templates/crochet/pattern.html:45
msgid "Guardado" msgid "Guardado"
msgstr "Saved" msgstr "Saved"
@@ -291,11 +289,6 @@ msgstr "Landscape"
msgid "Secciones" msgid "Secciones"
msgstr "Sections" msgstr "Sections"
#: crochet/templates/crochet/pattern.html:143
#: crochet/templates/crochet/pattern.html:189
msgid "Vista previa"
msgstr "Preview"
#: crochet/templates/crochet/pattern.html:152 #: crochet/templates/crochet/pattern.html:152
msgid "Colapsar todo" msgid "Colapsar todo"
msgstr "Collapse all" msgstr "Collapse all"
@@ -491,3 +484,12 @@ msgstr ""
#: crochet/views.py:24 #: crochet/views.py:24
msgid "Este patrón todavía no tiene contenido." msgid "Este patrón todavía no tiene contenido."
msgstr "This pattern doesn't have any content yet." msgstr "This pattern doesn't have any content yet."
#~ msgid "Idioma"
#~ msgstr "Language"
#~ msgid "Editar"
#~ msgstr "Edit"
#~ msgid "Eliminar patrón"
#~ msgstr "Delete pattern"
+19 -26
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: crochet\n" "Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-20 09:18+0000\n" "POT-Creation-Date: 2026-07-20 10:32+0000\n"
"Language: es\n" "Language: es\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -47,35 +47,33 @@ msgstr ""
msgid "¿Seguro que quieres eliminar este patrón? No podrás deshacerlo." msgid "¿Seguro que quieres eliminar este patrón? No podrás deshacerlo."
msgstr "" msgstr ""
#: crochet/templates/crochet/account_home.html:34 #: crochet/templates/crochet/account_home.html:37
msgid "Patrón sin título" msgid "Patrón sin título"
msgstr "" msgstr ""
#: crochet/templates/crochet/account_home.html:37 #: crochet/templates/crochet/account_home.html:41
#, python-format #, python-format
msgid "Actualizado el %(date)s" msgid "Actualizado el %(date)s"
msgstr "" msgstr ""
#: crochet/templates/crochet/account_home.html:40 #: crochet/templates/crochet/account_home.html:44
#: crochet/templates/crochet/pattern.html:33 #: crochet/templates/crochet/pattern.html:143
msgid "Ver patrón" #: crochet/templates/crochet/pattern.html:189
msgid "Vista previa"
msgstr "" msgstr ""
#: crochet/templates/crochet/account_home.html:41 #: crochet/templates/crochet/account_home.html:48
msgid "Editar" #: crochet/templates/crochet/pattern.html:38
msgstr ""
#: crochet/templates/crochet/account_home.html:45
msgid "Eliminar" msgid "Eliminar"
msgstr "" msgstr ""
#: crochet/templates/crochet/account_home.html:54 #: crochet/templates/crochet/account_home.html:57
msgid "Todavía no tienes ningún patrón." msgid "Todavía no tienes ningún patrón."
msgstr "" msgstr ""
#: crochet/templates/crochet/account_settings.html:4 #: crochet/templates/crochet/account_settings.html:4
#: crochet/templates/crochet/account_settings.html:18 #: crochet/templates/crochet/account_settings.html:18
#: crochet/templates/crochet/base.html:50 #: crochet/templates/crochet/base.html:52
msgid "Ajustes de la cuenta" msgid "Ajustes de la cuenta"
msgstr "" msgstr ""
@@ -84,21 +82,21 @@ msgid "Volver a mis patrones"
msgstr "" msgstr ""
#: crochet/templates/crochet/base.html:39 #: crochet/templates/crochet/base.html:39
msgid "Idioma" msgid "Cambiar idioma"
msgstr "" msgstr ""
#: crochet/templates/crochet/base.html:54 #: crochet/templates/crochet/base.html:56
msgid "Cerrar sesión" msgid "Cerrar sesión"
msgstr "" msgstr ""
#: crochet/templates/crochet/base.html:60 #: crochet/templates/crochet/base.html:62
#: crochet/templates/crochet/home.html:24 #: crochet/templates/crochet/home.html:24
#: crochet/templates/registration/login.html:4 #: crochet/templates/registration/login.html:4
#: crochet/templates/registration/login.html:14 #: crochet/templates/registration/login.html:14
msgid "Iniciar sesión" msgid "Iniciar sesión"
msgstr "" msgstr ""
#: crochet/templates/crochet/base.html:61 #: crochet/templates/crochet/base.html:63
#: crochet/templates/registration/register.html:29 #: crochet/templates/registration/register.html:29
msgid "Registrarme" msgid "Registrarme"
msgstr "" msgstr ""
@@ -163,12 +161,12 @@ msgstr ""
msgid "Guardar" msgid "Guardar"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:34 #: crochet/templates/crochet/pattern.html:33
msgid "Exportar a PDF" msgid "Ver patrón"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:38 #: crochet/templates/crochet/pattern.html:34
msgid "Eliminar patrón" msgid "Exportar a PDF"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:45 #: crochet/templates/crochet/pattern.html:45
@@ -282,11 +280,6 @@ msgstr ""
msgid "Secciones" msgid "Secciones"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:143
#: crochet/templates/crochet/pattern.html:189
msgid "Vista previa"
msgstr ""
#: crochet/templates/crochet/pattern.html:152 #: crochet/templates/crochet/pattern.html:152
msgid "Colapsar todo" msgid "Colapsar todo"
msgstr "" msgstr ""
+15 -12
View File
@@ -22,23 +22,26 @@
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{% for pattern in patterns %} {% for pattern in patterns %}
<div class="card bg-base-100 shadow border border-base-300"> <div class="card bg-base-100 shadow border border-base-300">
<figure class="h-40 bg-base-200"> <a href="{% url 'crochet:pattern_edit' pattern.uuid %}">
{% if pattern.cover_image_thumbnail %} <figure class="h-40 bg-base-200">
<img src="{{ pattern.cover_image_thumbnail.url }}" alt="" class="w-full h-full object-cover"> {% if pattern.cover_image_thumbnail %}
{% else %} <img src="{{ pattern.cover_image_thumbnail.url }}" alt="" class="w-full h-full object-cover">
<span class="text-4xl">🧶</span> {% else %}
{% endif %} <span class="text-4xl">🧶</span>
</figure> {% endif %}
</figure>
</a>
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-lg"> <a href="{% url 'crochet:pattern_edit' pattern.uuid %}">
{% if pattern.page_settings.title %}{{ pattern.page_settings.title }}{% else %}{% trans 'Patrón sin título' %}{% endif %} <h2 class="card-title text-lg">
</h2> {% if pattern.page_settings.title %}{{ pattern.page_settings.title }}{% else %}{% trans 'Patrón sin título' %}{% endif %}
</h2>
</a>
<p class="text-sm text-base-content/60"> <p class="text-sm text-base-content/60">
{% blocktrans with date=pattern.updated_at|date:'d/m/Y' %}Actualizado el {{ date }}{% endblocktrans %} {% blocktrans with date=pattern.updated_at|date:'d/m/Y' %}Actualizado el {{ date }}{% endblocktrans %}
</p> </p>
<div class="card-actions justify-end mt-2"> <div class="card-actions justify-end mt-2">
<a href="{% url 'crochet:pattern_detail' pattern.uuid %}" class="btn btn-sm btn-outline">{% trans 'Ver patrón' %}</a> <a href="{% url 'crochet:pattern_detail' pattern.uuid %}" class="btn btn-sm btn-outline">{% trans 'Vista previa' %}</a>
<a href="{% url 'crochet:pattern_edit' pattern.uuid %}" class="btn btn-sm btn-primary">{% trans 'Editar' %}</a>
<form method="post" action="{% url 'crochet:pattern_delete' pattern.uuid %}" <form method="post" action="{% url 'crochet:pattern_delete' pattern.uuid %}"
onsubmit="return confirm('{{ delete_confirm_message }}')"> onsubmit="return confirm('{{ delete_confirm_message }}')">
{% csrf_token %} {% csrf_token %}
+14 -12
View File
@@ -27,20 +27,22 @@
<a href="{% url 'crochet:home' %}" class="btn btn-ghost text-xl px-2">🧶 Crochet</a> <a href="{% url 'crochet:home' %}" class="btn btn-ghost text-xl px-2">🧶 Crochet</a>
</div> </div>
<div class="flex-none flex items-center gap-2"> <div class="flex-none flex items-center gap-2">
<!-- Cambia de idioma sin perder la página en la que estás: cada
ruta traducible (ver crochet/urls.py) cambia entera de un
idioma a otro, no solo el prefijo (/es/, /en/), así que no
basta con sustituirlo a mano en request.path (ver
crochet_i18n.translate_url). onchange en vez de un <form> normal
porque cada <option> lleva ya la URL completa de destino (no
hay un único action al que enviar un valor). -->
{% get_current_language as CURRENT_LANGUAGE %} {% get_current_language as CURRENT_LANGUAGE %}
{% get_available_languages as AVAILABLE_LANGUAGES %} {% get_available_languages as AVAILABLE_LANGUAGES %}
<select class="select select-sm select-bordered" onchange="window.location.href=this.value" aria-label="{% trans 'Idioma' %}"> <!-- El idioma es una preferencia de la página, no de la cuenta: va
{% for lang_code, lang_name in AVAILABLE_LANGUAGES %} siempre en el mismo sitio del navbar, tanto si has iniciado
<option value="{% translate_url request.path lang_code %}"{% if lang_code == CURRENT_LANGUAGE %} selected{% endif %}>{{ lang_name }}</option> sesión como si no, en vez de vivir dentro del dropdown de
{% endfor %} usuario (que no existe para anónimos). Cada opción es un <a>
</select> normal (no un <select>), así que navegar con teclado entre
ellas no dispara nada hasta que se pulsa/activa una. -->
<div class="dropdown dropdown-end">
<button type="button" tabindex="0" class="btn btn-sm btn-ghost" aria-label="{% trans 'Cambiar idioma' %}">{{ CURRENT_LANGUAGE|language_flag }} {{ CURRENT_LANGUAGE|upper }}</button>
<ul tabindex="0" class="dropdown-content menu menu-sm bg-base-100 border border-base-300 rounded-box z-10 w-40 p-2 shadow">
{% for lang_code, lang_name in AVAILABLE_LANGUAGES %}
<li><a href="{% translate_url request.path lang_code %}"{% if lang_code == CURRENT_LANGUAGE %} class="menu-active"{% endif %}>{{ lang_code|language_flag }} {{ lang_name }}</a></li>
{% endfor %}
</ul>
</div>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<!-- Dropdown CSS puro de daisyUI (sin JS propio): se abre con <!-- Dropdown CSS puro de daisyUI (sin JS propio): se abre con
:focus-within al pulsar/tabular al botón disparador. --> :focus-within al pulsar/tabular al botón disparador. -->
+1 -1
View File
@@ -35,7 +35,7 @@
<form method="post" action="{% url 'crochet:pattern_delete' pattern.uuid %}" <form method="post" action="{% url 'crochet:pattern_delete' pattern.uuid %}"
onsubmit="return confirm('{{ delete_confirm_message }}')"> onsubmit="return confirm('{{ delete_confirm_message }}')">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline btn-error">{% trans 'Eliminar patrón' %}</button> <button type="submit" class="btn btn-sm btn-outline btn-error">{% trans 'Eliminar' %}</button>
</form> </form>
</div> </div>
</header> </header>
+11
View File
@@ -3,6 +3,12 @@ from django.urls import translate_url as django_translate_url
register = template.Library() register = template.Library()
# Mismos idiomas que LANGUAGES en config/settings/base.py.
LANGUAGE_FLAGS = {
'es': '🇪🇸',
'en': '🇬🇧',
}
@register.simple_tag @register.simple_tag
def translate_url(path, lang_code): def translate_url(path, lang_code):
@@ -13,3 +19,8 @@ def translate_url(path, lang_code):
# prefijo (p.ej. /es/patron/<uuid>/editar/ frente a # prefijo (p.ej. /es/patron/<uuid>/editar/ frente a
# /en/pattern/<uuid>/edit/). # /en/pattern/<uuid>/edit/).
return django_translate_url(path, lang_code) return django_translate_url(path, lang_code)
@register.filter
def language_flag(lang_code):
return LANGUAGE_FLAGS.get(lang_code, '')
+37 -7
View File
@@ -56,7 +56,7 @@ class HomeViewTests(TestCase):
class LanguageSwitcherTests(TestCase): class LanguageSwitcherTests(TestCase):
def test_home_page_switcher_offers_the_same_page_in_english(self): def test_home_page_switcher_offers_the_same_page_in_english(self):
response = self.client.get('/es/') response = self.client.get('/es/')
self.assertContains(response, '<option value="/en/"') self.assertContains(response, 'href="/en/"')
def test_translated_route_switcher_preserves_the_current_view(self): def test_translated_route_switcher_preserves_the_current_view(self):
# No solo cambia el prefijo (/es/ -> /en/): toda la ruta traducible # No solo cambia el prefijo (/es/ -> /en/): toda la ruta traducible
@@ -71,12 +71,28 @@ class LanguageSwitcherTests(TestCase):
response = self.client.get(es_url) response = self.client.get(es_url)
self.assertContains(response, f'value="{en_url}"') self.assertContains(response, f'href="{en_url}"')
def test_current_language_is_preselected(self): def test_current_language_is_shown_on_the_trigger_button(self):
response = self.client.get('/es/') response = self.client.get('/es/')
self.assertContains(response, '<option value="/es/" selected>') self.assertContains(response, '🇪🇸 ES<')
self.assertContains(response, '<option value="/en/">')
def test_menu_options_show_a_flag_per_language(self):
response = self.client.get('/es/')
self.assertContains(response, '🇪🇸 Español')
self.assertContains(response, '🇬🇧 Inglés')
def test_switcher_is_shown_regardless_of_login_state(self):
# El idioma es una preferencia de la página, no de la cuenta: debe
# verse tanto si hay sesión iniciada como si no, no solo dentro del
# dropdown de usuario (que no existe para anónimos).
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
self.client.force_login(user)
response = self.client.get('/es/')
self.assertContains(response, '🇪🇸 ES<')
class NavbarAccountDropdownTests(TestCase): class NavbarAccountDropdownTests(TestCase):
@@ -94,10 +110,24 @@ class NavbarAccountDropdownTests(TestCase):
self.assertContains(response, f'href="{settings_url}"') self.assertContains(response, f'href="{settings_url}"')
self.assertContains(response, f'action="{logout_url}"') self.assertContains(response, f'action="{logout_url}"')
def test_anonymous_user_does_not_see_the_dropdown(self): def test_anonymous_user_does_not_see_the_account_dropdown(self):
with translation.override('es'):
settings_url = reverse('crochet:account_settings')
response = self.client.get('/es/') response = self.client.get('/es/')
self.assertNotContains(response, 'dropdown dropdown-end') # El dropdown de idioma sí está presente para anónimos; lo que no
# debe verse es el de la cuenta (ajustes/cerrar sesión).
self.assertNotContains(response, f'href="{settings_url}"')
self.assertEqual(response.content.decode().count('class="dropdown dropdown-end"'), 1)
def test_language_dropdown_is_separate_from_the_account_dropdown(self):
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
self.client.force_login(user)
response = self.client.get('/es/')
self.assertEqual(response.content.decode().count('class="dropdown dropdown-end"'), 2)
class PatternEditViewTests(TestCase): class PatternEditViewTests(TestCase):