diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py
index 53cf94ff4..6c29ac058 100644
--- a/bookwyrm/models/author.py
+++ b/bookwyrm/models/author.py
@@ -27,7 +27,7 @@ class Author(BookDataModel):
# idk probably other keys would be useful here?
born = fields.DateTimeField(blank=True, null=True)
died = fields.DateTimeField(blank=True, null=True)
- name = fields.CharField(max_length=255, deduplication_field=True)
+ name = fields.CharField(max_length=255)
aliases = fields.ArrayField(
models.CharField(max_length=255), blank=True, default=list
)
diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py
index 361079906..7d14f88f9 100644
--- a/bookwyrm/models/fields.py
+++ b/bookwyrm/models/fields.py
@@ -296,7 +296,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data, overwrite=True):
- """helper function for assinging a value to the field"""
+ """helper function for assigning a value to the field"""
if not overwrite and getattr(instance, self.name).exists():
return False
@@ -398,7 +398,11 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
if formatted is None or formatted is MISSING:
return False
- if not overwrite and hasattr(instance, self.name):
+ if (
+ not overwrite
+ and hasattr(instance, self.name)
+ and getattr(instance, self.name)
+ ):
return False
getattr(instance, self.name).save(*formatted, save=save)
diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py
index 8224a2787..32465d6ed 100644
--- a/bookwyrm/preview_images.py
+++ b/bookwyrm/preview_images.py
@@ -317,15 +317,21 @@ def save_and_cleanup(image, instance=None):
"""Save and close the file"""
if not isinstance(instance, (models.Book, models.User, models.SiteSettings)):
return False
- uuid = uuid4()
- file_name = f"{instance.id}-{uuid}.jpg"
image_buffer = BytesIO()
try:
try:
- old_path = instance.preview_image.name
+ file_name = instance.preview_image.name
except ValueError:
- old_path = None
+ file_name = None
+
+ if not file_name or file_name == "":
+ uuid = uuid4()
+ file_name = f"{instance.id}-{uuid}.jpg"
+
+ # Clean up old file before saving
+ if file_name and default_storage.exists(file_name):
+ default_storage.delete(file_name)
# Save
image.save(image_buffer, format="jpeg", quality=75)
@@ -345,10 +351,6 @@ def save_and_cleanup(image, instance=None):
else:
instance.save(update_fields=["preview_image"])
- # Clean up old file after saving
- if old_path and default_storage.exists(old_path):
- default_storage.delete(old_path)
-
finally:
image_buffer.close()
return True
diff --git a/bookwyrm/templates/author/author.html b/bookwyrm/templates/author/author.html
index 6a67b50b3..b066c6ca4 100644
--- a/bookwyrm/templates/author/author.html
+++ b/bookwyrm/templates/author/author.html
@@ -2,6 +2,7 @@
{% load i18n %}
{% load markdown %}
{% load humanize %}
+{% load utilities %}
{% block title %}{{ author.name }}{% endblock %}
@@ -25,7 +26,7 @@
- {% if author.aliases or author.born or author.died or author.wikipedia_link or author.openlibrary_key or author.inventaire_id %}
+ {% if author.aliases or author.born or author.died or author.wikipedia_link or author.openlibrary_key or author.inventaire_id or author.isni %}
@@ -63,6 +64,14 @@
{% endif %}
+ {% if author.isni %}
+
+
+ {% trans "View ISNI record" %}
+
+
+ {% endif %}
+
{% if author.openlibrary_key %}
diff --git a/bookwyrm/templates/book/edit/edit_book.html b/bookwyrm/templates/book/edit/edit_book.html
index fc11208fd..3d41058e3 100644
--- a/bookwyrm/templates/book/edit/edit_book.html
+++ b/bookwyrm/templates/book/edit/edit_book.html
@@ -1,6 +1,7 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load humanize %}
+{% load utilities %}
{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %}
@@ -52,19 +53,29 @@
{% for author in author_matches %}
- {% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %}
+ {% blocktrans with name=author.name %}Is "{{ name }}" one of these authors?{% endblocktrans %}
{% with forloop.counter0 as counter %}
{% for match in author.matches %}
-
+
{{ match.name }}
-
- {% blocktrans with book_title=match.book_set.first.title %}Author of {{ book_title }} {% endblocktrans %}
+
+ {% with book_title=match.book_set.first.title alt_title=match.bio %}
+ {% if book_title %}
+ {% trans "Author of " %}{{ book_title }}
+ {% else %}
+ {% if alt_title %}{% trans "Author of " %}{{ alt_title }} {% else %} {% trans "Find more information at isni.org" %}{% endif %}
+ {% endif %}
+ {% endwith %}
+
+ {{ author.existing_isnis|get_isni_bio:match }}
+
+ {{ author.existing_isnis|get_isni:match }}
{% endfor %}
-
+
{% trans "This is a new author" %}
{% endwith %}
diff --git a/bookwyrm/templatetags/utilities.py b/bookwyrm/templatetags/utilities.py
index d31f0e4d4..5cc25fed8 100644
--- a/bookwyrm/templatetags/utilities.py
+++ b/bookwyrm/templatetags/utilities.py
@@ -1,8 +1,11 @@
""" template filters for really common utilities """
import os
+import re
from uuid import uuid4
from django import template
+from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
+from django.template.defaultfilters import stringfilter
from django.templatetags.static import static
@@ -66,3 +69,39 @@ def get_book_cover_thumbnail(book, size="medium", ext="jpg"):
return cover_thumbnail.url
except OSError:
return static("images/no_cover.jpg")
+
+
+@register.filter(name="get_isni_bio")
+def get_isni_bio(existing, author):
+ """Returns the isni bio string if an existing author has an isni listed"""
+ auth_isni = re.sub(r"\D", "", str(author.isni))
+ if len(existing) == 0:
+ return ""
+ for value in existing:
+ if hasattr(value, "bio") and auth_isni == re.sub(r"\D", "", str(value.isni)):
+ return mark_safe(f"Author of {value.bio} ")
+
+ return ""
+
+
+# pylint: disable=unused-argument
+@register.filter(name="get_isni", needs_autoescape=True)
+def get_isni(existing, author, autoescape=True):
+ """Returns the isni ID if an existing author has an ISNI listing"""
+ auth_isni = re.sub(r"\D", "", str(author.isni))
+ if len(existing) == 0:
+ return ""
+ for value in existing:
+ if hasattr(value, "isni") and auth_isni == re.sub(r"\D", "", str(value.isni)):
+ isni = value.isni
+ return mark_safe(
+ f' '
+ )
+ return ""
+
+
+@register.filter(name="remove_spaces")
+@stringfilter
+def remove_spaces(arg):
+ """Removes spaces from argument passed in"""
+ return re.sub(r"\s", "", str(arg))
diff --git a/bookwyrm/tests/models/test_fields.py b/bookwyrm/tests/models/test_fields.py
index 74f4c48bd..8028a305e 100644
--- a/bookwyrm/tests/models/test_fields.py
+++ b/bookwyrm/tests/models/test_fields.py
@@ -19,7 +19,7 @@ from django.utils import timezone
from bookwyrm import activitypub
from bookwyrm.activitypub.base_activity import ActivityObject
-from bookwyrm.models import fields, User, Status
+from bookwyrm.models import fields, User, Status, Edition
from bookwyrm.models.base_model import BookWyrmModel
from bookwyrm.models.activitypub_mixin import ActivitypubMixin
from bookwyrm.settings import DOMAIN
@@ -215,7 +215,7 @@ class ModelFields(TestCase):
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
public = "https://www.w3.org/ns/activitystreams#Public"
- followers = "%s/followers" % user.remote_id
+ followers = f"{user.remote_id}/followers"
instance = fields.PrivacyField()
instance.name = "privacy_field"
@@ -409,11 +409,10 @@ class ModelFields(TestCase):
"""loadin' a list of items from Links"""
# TODO
- @responses.activate
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
@patch("bookwyrm.suggested_users.remove_user_task.delay")
- def test_image_field(self, *_):
- """storing images"""
+ def test_image_field_to_activity(self, *_):
+ """serialize an image field to activitypub"""
user = User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
@@ -437,16 +436,155 @@ class ModelFields(TestCase):
self.assertEqual(output.name, "")
self.assertEqual(output.type, "Document")
+ @responses.activate
+ def test_image_field_from_activity(self, *_):
+ """load an image from activitypub"""
+ image_file = pathlib.Path(__file__).parent.joinpath(
+ "../../static/images/default_avi.jpg"
+ )
+ image = Image.open(image_file)
+ output = BytesIO()
+ image.save(output, format=image.format)
+
+ instance = fields.ImageField()
+
responses.add(
responses.GET,
"http://www.example.com/image.jpg",
- body=user.avatar.file.read(),
+ body=image.tobytes(),
status=200,
)
loaded_image = instance.field_from_activity("http://www.example.com/image.jpg")
self.assertIsInstance(loaded_image, list)
self.assertIsInstance(loaded_image[1], ContentFile)
+ @responses.activate
+ def test_image_field_set_field_from_activity(self, *_):
+ """update a model instance from an activitypub object"""
+ image_file = pathlib.Path(__file__).parent.joinpath(
+ "../../static/images/default_avi.jpg"
+ )
+ image = Image.open(image_file)
+ output = BytesIO()
+ image.save(output, format=image.format)
+
+ instance = fields.ImageField(activitypub_field="cover", name="cover")
+
+ responses.add(
+ responses.GET,
+ "http://www.example.com/image.jpg",
+ body=image.tobytes(),
+ status=200,
+ )
+ book = Edition.objects.create(title="hello")
+
+ MockActivity = namedtuple("MockActivity", ("cover"))
+ mock_activity = MockActivity("http://www.example.com/image.jpg")
+
+ instance.set_field_from_activity(book, mock_activity)
+ self.assertIsNotNone(book.cover.name)
+ self.assertEqual(book.cover.size, 43200)
+
+ @responses.activate
+ def test_image_field_set_field_from_activity_no_overwrite_no_cover(self, *_):
+ """update a model instance from an activitypub object"""
+ image_file = pathlib.Path(__file__).parent.joinpath(
+ "../../static/images/default_avi.jpg"
+ )
+ image = Image.open(image_file)
+ output = BytesIO()
+ image.save(output, format=image.format)
+
+ instance = fields.ImageField(activitypub_field="cover", name="cover")
+
+ responses.add(
+ responses.GET,
+ "http://www.example.com/image.jpg",
+ body=image.tobytes(),
+ status=200,
+ )
+ book = Edition.objects.create(title="hello")
+
+ MockActivity = namedtuple("MockActivity", ("cover"))
+ mock_activity = MockActivity("http://www.example.com/image.jpg")
+
+ instance.set_field_from_activity(book, mock_activity, overwrite=False)
+ self.assertIsNotNone(book.cover.name)
+ self.assertEqual(book.cover.size, 43200)
+
+ @responses.activate
+ def test_image_field_set_field_from_activity_no_overwrite_with_cover(self, *_):
+ """update a model instance from an activitypub object"""
+ image_file = pathlib.Path(__file__).parent.joinpath(
+ "../../static/images/default_avi.jpg"
+ )
+ image = Image.open(image_file)
+ output = BytesIO()
+ image.save(output, format=image.format)
+
+ another_image_file = pathlib.Path(__file__).parent.joinpath(
+ "../../static/images/logo.png"
+ )
+ another_image = Image.open(another_image_file)
+ another_output = BytesIO()
+ another_image.save(another_output, format=another_image.format)
+
+ instance = fields.ImageField(activitypub_field="cover", name="cover")
+
+ responses.add(
+ responses.GET,
+ "http://www.example.com/image.jpg",
+ body=another_image.tobytes(),
+ status=200,
+ )
+ book = Edition.objects.create(title="hello")
+ book.cover.save("test.jpg", ContentFile(output.getvalue()))
+ self.assertEqual(book.cover.size, 2136)
+
+ MockActivity = namedtuple("MockActivity", ("cover"))
+ mock_activity = MockActivity("http://www.example.com/image.jpg")
+
+ instance.set_field_from_activity(book, mock_activity, overwrite=False)
+ # same cover as before
+ self.assertEqual(book.cover.size, 2136)
+
+ @responses.activate
+ def test_image_field_set_field_from_activity_with_overwrite_with_cover(self, *_):
+ """update a model instance from an activitypub object"""
+ image_file = pathlib.Path(__file__).parent.joinpath(
+ "../../static/images/default_avi.jpg"
+ )
+ image = Image.open(image_file)
+ output = BytesIO()
+ image.save(output, format=image.format)
+ book = Edition.objects.create(title="hello")
+ book.cover.save("test.jpg", ContentFile(output.getvalue()))
+ self.assertEqual(book.cover.size, 2136)
+
+ another_image_file = pathlib.Path(__file__).parent.joinpath(
+ "../../static/images/logo.png"
+ )
+ another_image = Image.open(another_image_file)
+ another_output = BytesIO()
+ another_image.save(another_output, format=another_image.format)
+
+ instance = fields.ImageField(activitypub_field="cover", name="cover")
+
+ responses.add(
+ responses.GET,
+ "http://www.example.com/image.jpg",
+ body=another_image.tobytes(),
+ status=200,
+ )
+
+ MockActivity = namedtuple("MockActivity", ("cover"))
+ mock_activity = MockActivity("http://www.example.com/image.jpg")
+
+ instance.set_field_from_activity(book, mock_activity, overwrite=True)
+ # new cover
+ self.assertIsNotNone(book.cover.name)
+ self.assertEqual(book.cover.size, 376800)
+
def test_datetime_field(self, *_):
"""this one is pretty simple, it just has to use isoformat"""
instance = fields.DateTimeField()
diff --git a/bookwyrm/utils/isni.py b/bookwyrm/utils/isni.py
new file mode 100644
index 000000000..a35c3f249
--- /dev/null
+++ b/bookwyrm/utils/isni.py
@@ -0,0 +1,183 @@
+"""ISNI author checking utilities"""
+import xml.etree.ElementTree as ET
+import requests
+
+from bookwyrm import activitypub, models
+
+
+def request_isni_data(search_index, search_term, max_records=5):
+ """Request data from the ISNI API"""
+
+ search_string = f'{search_index}="{search_term}"'
+ query_params = {
+ "query": search_string,
+ "version": "1.1",
+ "operation": "searchRetrieve",
+ "recordSchema": "isni-b",
+ "maximumRecords": max_records,
+ "startRecord": "1",
+ "recordPacking": "xml",
+ "sortKeys": "RLV,pica,0,,",
+ }
+ result = requests.get("http://isni.oclc.org/sru/", params=query_params, timeout=10)
+ # the OCLC ISNI server asserts the payload is encoded
+ # in latin1, but we know better
+ result.encoding = "utf-8"
+ return result.text
+
+
+def make_name_string(element):
+ """create a string of form 'personal_name surname'"""
+
+ # NOTE: this will often be incorrect, many naming systems
+ # list "surname" before personal name
+ forename = element.find(".//forename")
+ surname = element.find(".//surname")
+ if forename is not None:
+ return "".join([forename.text, " ", surname.text])
+ return surname.text
+
+
+def get_other_identifier(element, code):
+ """Get other identifiers associated with an author from their ISNI record"""
+
+ identifiers = element.findall(".//otherIdentifierOfIdentity")
+ for section_head in identifiers:
+ if (
+ section_head.find(".//type") is not None
+ and section_head.find(".//type").text == code
+ and section_head.find(".//identifier") is not None
+ ):
+ return section_head.find(".//identifier").text
+
+ # if we can't find it in otherIdentifierOfIdentity,
+ # try sources
+ for source in element.findall(".//sources"):
+ code_of_source = source.find(".//codeOfSource")
+ if code_of_source is not None and code_of_source.text.lower() == code.lower():
+ return source.find(".//sourceIdentifier").text
+
+ return ""
+
+
+def get_external_information_uri(element, match_string):
+ """Get URLs associated with an author from their ISNI record"""
+
+ sources = element.findall(".//externalInformation")
+ for source in sources:
+ information = source.find(".//information")
+ uri = source.find(".//URI")
+ if (
+ uri is not None
+ and information is not None
+ and information.text.lower() == match_string.lower()
+ ):
+ return uri.text
+ return ""
+
+
+def find_authors_by_name(name_string, description=False):
+ """Query the ISNI database for possible author matches by name"""
+
+ payload = request_isni_data("pica.na", name_string)
+ # parse xml
+ root = ET.fromstring(payload)
+ # build list of possible authors
+ possible_authors = []
+ for element in root.iter("responseRecord"):
+ personal_name = element.find(".//forename/..")
+ if not personal_name:
+ continue
+
+ author = get_author_from_isni(element.find(".//isniUnformatted").text)
+
+ if bool(description):
+
+ titles = []
+ # prefer title records from LoC+ coop, Australia, Ireland, or Singapore
+ # in that order
+ for source in ["LCNACO", "NLA", "N6I", "NLB"]:
+ for parent in element.findall(f'.//titleOfWork/[@source="{source}"]'):
+ titles.append(parent.find(".//title"))
+ for parent in element.findall(f'.//titleOfWork[@subsource="{source}"]'):
+ titles.append(parent.find(".//title"))
+ # otherwise just grab the first title listing
+ titles.append(element.find(".//title"))
+
+ if titles is not None:
+ # some of the "titles" in ISNI are a little ...iffy
+ # '@' is used by ISNI/OCLC to index the starting point ignoring stop words
+ # (e.g. "The @Government of no one")
+ title_elements = [
+ e for e in titles if not e.text.replace("@", "").isnumeric()
+ ]
+ if len(title_elements):
+ author.bio = title_elements[0].text.replace("@", "")
+ else:
+ author.bio = None
+
+ possible_authors.append(author)
+
+ return possible_authors
+
+
+def get_author_from_isni(isni):
+ """Find data to populate a new author record from their ISNI"""
+
+ payload = request_isni_data("pica.isn", isni)
+ # parse xml
+ root = ET.fromstring(payload)
+ # there should only be a single responseRecord
+ # but let's use the first one just in case
+ element = root.find(".//responseRecord")
+ name = make_name_string(element.find(".//forename/.."))
+ viaf = get_other_identifier(element, "viaf")
+ # use a set to dedupe aliases in ISNI
+ aliases = set()
+ aliases_element = element.findall(".//personalNameVariant")
+ for entry in aliases_element:
+ aliases.add(make_name_string(entry))
+ # aliases needs to be list not set
+ aliases = list(aliases)
+ bio = element.find(".//nameTitle")
+ bio = bio.text if bio is not None else ""
+ wikipedia = get_external_information_uri(element, "Wikipedia")
+
+ author = activitypub.Author(
+ id=element.find(".//isniURI").text,
+ name=name,
+ isni=isni,
+ viafId=viaf,
+ aliases=aliases,
+ bio=bio,
+ wikipediaLink=wikipedia,
+ )
+
+ return author
+
+
+def build_author_from_isni(match_value):
+ """Build basic author class object from ISNI URL"""
+
+ # if it is an isni value get the data
+ if match_value.startswith("https://isni.org/isni/"):
+ isni = match_value.replace("https://isni.org/isni/", "")
+ return {"author": get_author_from_isni(isni)}
+ # otherwise it's a name string
+ return {}
+
+
+def augment_author_metadata(author, isni):
+ """Update any missing author fields from ISNI data"""
+
+ isni_author = get_author_from_isni(isni)
+ isni_author.to_model(model=models.Author, instance=author, overwrite=False)
+
+ # we DO want to overwrite aliases because we're adding them to the
+ # existing aliases and ISNI will usually have more.
+ # We need to dedupe because ISNI records often have lots of dupe aliases
+ aliases = set(isni_author.aliases)
+ for alias in author.aliases:
+ aliases.add(alias)
+ author.aliases = list(aliases)
+ author.save()
diff --git a/bookwyrm/views/books/edit_book.py b/bookwyrm/views/books/edit_book.py
index 917271fcb..930becac2 100644
--- a/bookwyrm/views/books/edit_book.py
+++ b/bookwyrm/views/books/edit_book.py
@@ -1,4 +1,5 @@
""" the good stuff! the books! """
+from re import sub
from dateutil.parser import parse as dateparse
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import SearchRank, SearchVector
@@ -11,10 +12,16 @@ from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import book_search, forms, models
+
+# from bookwyrm.activitypub.base_activity import ActivityObject
+from bookwyrm.utils.isni import (
+ find_authors_by_name,
+ build_author_from_isni,
+ augment_author_metadata,
+)
from bookwyrm.views.helpers import get_edition
from .books import set_cover_from_url
-
# pylint: disable=no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
@@ -33,6 +40,7 @@ class EditBook(View):
data = {"book": book, "form": forms.EditionForm(instance=book)}
return TemplateResponse(request, "book/edit/edit_book.html", data)
+ # pylint: disable=too-many-locals
def post(self, request, book_id=None):
"""edit a book cool"""
# returns None if no match is found
@@ -48,6 +56,8 @@ class EditBook(View):
if add_author:
data["add_author"] = add_author
data["author_matches"] = []
+ data["isni_matches"] = []
+
for author in add_author:
if not author:
continue
@@ -56,15 +66,35 @@ class EditBook(View):
"aliases", weight="B"
)
+ author_matches = (
+ models.Author.objects.annotate(search=vector)
+ .annotate(rank=SearchRank(vector, author))
+ .filter(rank__gt=0.4)
+ .order_by("-rank")[:5]
+ )
+
+ isni_authors = find_authors_by_name(
+ author, description=True
+ ) # find matches from ISNI API
+
+ # dedupe isni authors we already have in the DB
+ exists = [
+ i
+ for i in isni_authors
+ for a in author_matches
+ if sub(r"\D", "", str(i.isni)) == sub(r"\D", "", str(a.isni))
+ ]
+
+ # pylint: disable=cell-var-from-loop
+ matches = list(filter(lambda x: x not in exists, isni_authors))
+ # combine existing and isni authors
+ matches.extend(author_matches)
+
data["author_matches"].append(
{
"name": author.strip(),
- "matches": (
- models.Author.objects.annotate(search=vector)
- .annotate(rank=SearchRank(vector, author))
- .filter(rank__gt=0.4)
- .order_by("-rank")[:5]
- ),
+ "matches": matches,
+ "existing_isnis": exists,
}
)
@@ -122,6 +152,8 @@ class EditBook(View):
class ConfirmEditBook(View):
"""confirm edits to a book"""
+ # pylint: disable=too-many-locals
+ # pylint: disable=too-many-branches
def post(self, request, book_id=None):
"""edit a book cool"""
# returns None if no match is found
@@ -147,9 +179,25 @@ class ConfirmEditBook(View):
author = get_object_or_404(
models.Author, id=request.POST[f"author_match-{i}"]
)
+ # update author metadata if the ISNI record is more complete
+ isni = request.POST.get(f"isni-for-{match}", None)
+ if isni is not None:
+ augment_author_metadata(author, isni)
except ValueError:
- # otherwise it's a name
- author = models.Author.objects.create(name=match)
+ # otherwise it's a new author
+ isni_match = request.POST.get(f"author_match-{i}")
+ author_object = build_author_from_isni(isni_match)
+ # with author data class from isni id
+ if "author" in author_object:
+ skeleton = models.Author.objects.create(
+ name=author_object["author"].name
+ )
+ author = author_object["author"].to_model(
+ model=models.Author, overwrite=True, instance=skeleton
+ )
+ else:
+ # or it's just a name
+ author = models.Author.objects.create(name=match)
book.authors.add(author)
# create work, if needed
diff --git a/locale/de_DE/LC_MESSAGES/django.mo b/locale/de_DE/LC_MESSAGES/django.mo
index fe402ad72..4ce83f72b 100644
Binary files a/locale/de_DE/LC_MESSAGES/django.mo and b/locale/de_DE/LC_MESSAGES/django.mo differ
diff --git a/locale/es_ES/LC_MESSAGES/django.mo b/locale/es_ES/LC_MESSAGES/django.mo
index fb8399c41..7186a12e4 100644
Binary files a/locale/es_ES/LC_MESSAGES/django.mo and b/locale/es_ES/LC_MESSAGES/django.mo differ
diff --git a/locale/es_ES/LC_MESSAGES/django.po b/locale/es_ES/LC_MESSAGES/django.po
index 0edb2f9f3..5a4114505 100644
--- a/locale/es_ES/LC_MESSAGES/django.po
+++ b/locale/es_ES/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-17 18:03+0000\n"
-"PO-Revision-Date: 2021-11-17 18:42\n"
+"PO-Revision-Date: 2021-11-19 18:53\n"
"Last-Translator: Mouse Reeve \n"
"Language-Team: Spanish\n"
"Language: es\n"
@@ -184,7 +184,7 @@ msgstr "Español"
#: bookwyrm/settings.py:168
msgid "Galego (Galician)"
-msgstr ""
+msgstr "Gallego (Galicia)"
#: bookwyrm/settings.py:169
msgid "Français (French)"
@@ -192,7 +192,7 @@ msgstr "Français (Francés)"
#: bookwyrm/settings.py:170
msgid "Lietuvių (Lithuanian)"
-msgstr ""
+msgstr "Lituano (Lituania)"
#: bookwyrm/settings.py:171
msgid "Português - Brasil (Brazilian Portuguese)"
@@ -904,17 +904,17 @@ msgstr "Todos los usuarios conocidos"
#: bookwyrm/templates/discover/card-header.html:8
#, python-format
msgid "%(username)s wants to read %(book_title)s "
-msgstr ""
+msgstr "%(username)s quiere leer %(book_title)s "
#: bookwyrm/templates/discover/card-header.html:13
#, python-format
msgid "%(username)s finished reading %(book_title)s "
-msgstr ""
+msgstr "%(username)s ha terminado de leer %(book_title)s "
#: bookwyrm/templates/discover/card-header.html:18
#, python-format
msgid "%(username)s started reading %(book_title)s "
-msgstr ""
+msgstr "%(username)s ha empezado a leer %(book_title)s "
#: bookwyrm/templates/discover/card-header.html:23
#, python-format
@@ -1400,11 +1400,11 @@ msgstr "Importar estado"
#: bookwyrm/templates/import/import_status.html:13
#: bookwyrm/templates/import/import_status.html:27
msgid "Retry Status"
-msgstr ""
+msgstr "Estado del Reintento"
#: bookwyrm/templates/import/import_status.html:22
msgid "Imports"
-msgstr ""
+msgstr "Importaciones"
#: bookwyrm/templates/import/import_status.html:39
msgid "Import started:"
@@ -1412,38 +1412,38 @@ msgstr "Importación ha empezado:"
#: bookwyrm/templates/import/import_status.html:48
msgid "In progress"
-msgstr ""
+msgstr "En progreso"
#: bookwyrm/templates/import/import_status.html:50
msgid "Refresh"
-msgstr ""
+msgstr "Refrescar"
#: bookwyrm/templates/import/import_status.html:71
#, python-format
msgid "%(display_counter)s item needs manual approval."
msgid_plural "%(display_counter)s items need manual approval."
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(display_counter)s elemento necesita aprobación manual."
+msgstr[1] "%(display_counter)s elementos necesitan aprobación manual."
#: bookwyrm/templates/import/import_status.html:76
#: bookwyrm/templates/import/manual_review.html:8
msgid "Review items"
-msgstr ""
+msgstr "Revisar elementos"
#: bookwyrm/templates/import/import_status.html:82
#, python-format
msgid "%(display_counter)s item failed to import."
msgid_plural "%(display_counter)s items failed to import."
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(display_counter)s elemento no se pudo importar."
+msgstr[1] "%(display_counter)s elementos no se pudieron importar."
#: bookwyrm/templates/import/import_status.html:88
msgid "View and troubleshoot failed items"
-msgstr ""
+msgstr "Ver y solucionar los elementos fallidos"
#: bookwyrm/templates/import/import_status.html:100
msgid "Row"
-msgstr ""
+msgstr "Fila"
#: bookwyrm/templates/import/import_status.html:103
#: bookwyrm/templates/shelf/shelf.html:141
@@ -1453,7 +1453,7 @@ msgstr "Título"
#: bookwyrm/templates/import/import_status.html:106
msgid "ISBN"
-msgstr ""
+msgstr "ISBN"
#: bookwyrm/templates/import/import_status.html:109
#: bookwyrm/templates/shelf/shelf.html:142
@@ -1463,7 +1463,7 @@ msgstr "Autor/Autora"
#: bookwyrm/templates/import/import_status.html:112
msgid "Shelf"
-msgstr ""
+msgstr "Estantería"
#: bookwyrm/templates/import/import_status.html:115
#: bookwyrm/templates/import/manual_review.html:13
@@ -1487,11 +1487,11 @@ msgstr "Estado"
#: bookwyrm/templates/import/import_status.html:130
msgid "Import preview unavailable."
-msgstr ""
+msgstr "Previsualización de la importación no disponible."
#: bookwyrm/templates/import/import_status.html:162
msgid "View imported review"
-msgstr ""
+msgstr "Ver reseña importada"
#: bookwyrm/templates/import/import_status.html:176
msgid "Imported"
@@ -1499,28 +1499,28 @@ msgstr "Importado"
#: bookwyrm/templates/import/import_status.html:182
msgid "Needs manual review"
-msgstr ""
+msgstr "Necesita revisión manual"
#: bookwyrm/templates/import/import_status.html:195
msgid "Retry"
-msgstr ""
+msgstr "Reintentar"
#: bookwyrm/templates/import/import_status.html:213
msgid "This import is in an old format that is no longer supported. If you would like to troubleshoot missing items from this import, click the button below to update the import format."
-msgstr ""
+msgstr "Esta importación está en un formato antiguo que ya no es compatible. Si desea solucionar los elementos que faltan en esta importación, haga clic en el botón de abajo para actualizar el formato de importación."
#: bookwyrm/templates/import/import_status.html:215
msgid "Update import"
-msgstr ""
+msgstr "Actualizar importación"
#: bookwyrm/templates/import/manual_review.html:5
#: bookwyrm/templates/import/troubleshoot.html:4
msgid "Import Troubleshooting"
-msgstr ""
+msgstr "Solucionar Importación"
#: bookwyrm/templates/import/manual_review.html:21
msgid "Approving a suggestion will permanently add the suggested book to your shelves and associate your reading dates, reviews, and ratings with that book."
-msgstr ""
+msgstr "La aprobación de una sugerencia añadirá permanentemente el libro sugerido a tus estanterías y asociará tus fechas de lectura, tus reseñas y tus valoraciones a ese libro."
#: bookwyrm/templates/import/manual_review.html:58
#: bookwyrm/templates/lists/curate.html:57
@@ -1529,7 +1529,7 @@ msgstr "Aprobar"
#: bookwyrm/templates/import/manual_review.html:66
msgid "Reject"
-msgstr ""
+msgstr "Rechazar"
#: bookwyrm/templates/import/tooltip.html:6
msgid "You can download your Goodreads data from the Import/Export page of your Goodreads account."
@@ -1537,31 +1537,31 @@ msgstr "Puede descargar sus datos de Goodreads desde la open an issue if you are seeing unexpected failed items."
-msgstr ""
+msgstr "Póngase en contacto con su administrador o cree una propuesta si está viendo elementos fallidos inesperados."
#: bookwyrm/templates/landing/about.html:7 bookwyrm/templates/layout.html:230
#, python-format
diff --git a/locale/fr_FR/LC_MESSAGES/django.mo b/locale/fr_FR/LC_MESSAGES/django.mo
index 34b8aeeda..4cdcbf8ea 100644
Binary files a/locale/fr_FR/LC_MESSAGES/django.mo and b/locale/fr_FR/LC_MESSAGES/django.mo differ
diff --git a/locale/fr_FR/LC_MESSAGES/django.po b/locale/fr_FR/LC_MESSAGES/django.po
index 25db17d99..fc1ecf41c 100644
--- a/locale/fr_FR/LC_MESSAGES/django.po
+++ b/locale/fr_FR/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-17 18:03+0000\n"
-"PO-Revision-Date: 2021-11-17 18:41\n"
+"PO-Revision-Date: 2021-11-22 19:37\n"
"Last-Translator: Mouse Reeve \n"
"Language-Team: French\n"
"Language: fr\n"
@@ -99,7 +99,7 @@ msgstr "Suppression du modérateur"
#: bookwyrm/models/base_model.py:21
msgid "Domain block"
-msgstr "Bloc de domaine"
+msgstr "Blocage de domaine"
#: bookwyrm/models/book.py:233
msgid "Audiobook"
@@ -184,7 +184,7 @@ msgstr "Español"
#: bookwyrm/settings.py:168
msgid "Galego (Galician)"
-msgstr ""
+msgstr "Galego (Galicien)"
#: bookwyrm/settings.py:169
msgid "Français (French)"
@@ -192,7 +192,7 @@ msgstr "Français"
#: bookwyrm/settings.py:170
msgid "Lietuvių (Lithuanian)"
-msgstr ""
+msgstr "Lietuvių (Lituanien)"
#: bookwyrm/settings.py:171
msgid "Português - Brasil (Brazilian Portuguese)"
@@ -1487,7 +1487,7 @@ msgstr "Statut"
#: bookwyrm/templates/import/import_status.html:130
msgid "Import preview unavailable."
-msgstr ""
+msgstr "Aperçu de l'importation indisponible."
#: bookwyrm/templates/import/import_status.html:162
msgid "View imported review"
@@ -1503,15 +1503,15 @@ msgstr "Nécessite une vérification manuelle"
#: bookwyrm/templates/import/import_status.html:195
msgid "Retry"
-msgstr ""
+msgstr "Réessayer"
#: bookwyrm/templates/import/import_status.html:213
msgid "This import is in an old format that is no longer supported. If you would like to troubleshoot missing items from this import, click the button below to update the import format."
-msgstr ""
+msgstr "Cette importation est dans un ancien format qui n'est plus pris en charge. Si vous souhaitez corriger les éléments manquants de cette importation, cliquez sur le bouton ci-dessous pour mettre à jour le format d'importation."
#: bookwyrm/templates/import/import_status.html:215
msgid "Update import"
-msgstr ""
+msgstr "Mettre à jour l'importation"
#: bookwyrm/templates/import/manual_review.html:5
#: bookwyrm/templates/import/troubleshoot.html:4
diff --git a/locale/gl_ES/LC_MESSAGES/django.mo b/locale/gl_ES/LC_MESSAGES/django.mo
index ad56b3371..c8999f804 100644
Binary files a/locale/gl_ES/LC_MESSAGES/django.mo and b/locale/gl_ES/LC_MESSAGES/django.mo differ
diff --git a/locale/gl_ES/LC_MESSAGES/django.po b/locale/gl_ES/LC_MESSAGES/django.po
index 1008b2d46..958563626 100644
--- a/locale/gl_ES/LC_MESSAGES/django.po
+++ b/locale/gl_ES/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-17 18:03+0000\n"
-"PO-Revision-Date: 2021-11-19 16:48\n"
+"PO-Revision-Date: 2021-11-19 17:43\n"
"Last-Translator: Mouse Reeve \n"
"Language-Team: Galician\n"
"Language: gl\n"
@@ -3114,140 +3114,140 @@ msgstr "Crear estante"
#, python-format
msgid "%(formatted_count)s book"
msgid_plural "%(formatted_count)s books"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(formatted_count)s libro"
+msgstr[1] "%(formatted_count)s libros"
#: bookwyrm/templates/shelf/shelf.html:97
#, python-format
msgid "(showing %(start)s-%(end)s)"
-msgstr ""
+msgstr "(mostrando %(start)s-%(end)s)"
#: bookwyrm/templates/shelf/shelf.html:109
msgid "Edit shelf"
-msgstr ""
+msgstr "Editar estante"
#: bookwyrm/templates/shelf/shelf.html:117
msgid "Delete shelf"
-msgstr ""
+msgstr "Eliminar estante"
#: bookwyrm/templates/shelf/shelf.html:145
#: bookwyrm/templates/shelf/shelf.html:171
msgid "Shelved"
-msgstr ""
+msgstr "No estante"
#: bookwyrm/templates/shelf/shelf.html:146
#: bookwyrm/templates/shelf/shelf.html:174
msgid "Started"
-msgstr ""
+msgstr "Comezado"
#: bookwyrm/templates/shelf/shelf.html:147
#: bookwyrm/templates/shelf/shelf.html:177
msgid "Finished"
-msgstr ""
+msgstr "Rematado"
#: bookwyrm/templates/shelf/shelf.html:203
msgid "This shelf is empty."
-msgstr ""
+msgstr "Este estante esta baleiro."
#: bookwyrm/templates/snippets/add_to_group_button.html:15
msgid "Invite"
-msgstr ""
+msgstr "Convidar"
#: bookwyrm/templates/snippets/add_to_group_button.html:24
msgid "Uninvite"
-msgstr ""
+msgstr "Retirar convite"
#: bookwyrm/templates/snippets/add_to_group_button.html:28
#, python-format
msgid "Remove @%(username)s"
-msgstr ""
+msgstr "Eliminar @%(username)s"
#: bookwyrm/templates/snippets/announcement.html:31
#, python-format
msgid "Posted by %(username)s "
-msgstr ""
+msgstr "Publicado por %(username)s "
#: bookwyrm/templates/snippets/authors.html:22
#, python-format
msgid "and %(remainder_count_display)s other"
msgid_plural "and %(remainder_count_display)s others"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "e %(remainder_count_display)s outro"
+msgstr[1] "e %(remainder_count_display)s outros"
#: bookwyrm/templates/snippets/book_cover.html:61
msgid "No cover"
-msgstr ""
+msgstr "Sen portada"
#: bookwyrm/templates/snippets/book_titleby.html:6
#, python-format
msgid "%(title)s by"
-msgstr ""
+msgstr "%(title)s por"
#: bookwyrm/templates/snippets/boost_button.html:20
#: bookwyrm/templates/snippets/boost_button.html:21
msgid "Boost"
-msgstr ""
+msgstr "Promover"
#: bookwyrm/templates/snippets/boost_button.html:33
#: bookwyrm/templates/snippets/boost_button.html:34
msgid "Un-boost"
-msgstr ""
+msgstr "Retirar promoción"
#: bookwyrm/templates/snippets/create_status.html:39
msgid "Quote"
-msgstr ""
+msgstr "Cita"
#: bookwyrm/templates/snippets/create_status/comment.html:15
msgid "Some thoughts on the book"
-msgstr ""
+msgstr "Cousas interesantes no libro"
#: bookwyrm/templates/snippets/create_status/comment.html:27
#: bookwyrm/templates/snippets/reading_modals/progress_update_modal.html:15
msgid "Progress:"
-msgstr ""
+msgstr "Progreso:"
#: bookwyrm/templates/snippets/create_status/comment.html:53
#: bookwyrm/templates/snippets/progress_field.html:18
msgid "pages"
-msgstr ""
+msgstr "páxinas"
#: bookwyrm/templates/snippets/create_status/comment.html:59
#: bookwyrm/templates/snippets/progress_field.html:23
msgid "percent"
-msgstr ""
+msgstr "porcentaxe"
#: bookwyrm/templates/snippets/create_status/comment.html:66
#, python-format
msgid "of %(pages)s pages"
-msgstr ""
+msgstr "de %(pages)s páxinas"
#: bookwyrm/templates/snippets/create_status/content_field.html:17
#: bookwyrm/templates/snippets/status/layout.html:34
#: bookwyrm/templates/snippets/status/layout.html:52
#: bookwyrm/templates/snippets/status/layout.html:53
msgid "Reply"
-msgstr ""
+msgstr "Responder"
#: bookwyrm/templates/snippets/create_status/content_field.html:17
msgid "Content"
-msgstr ""
+msgstr "Contido"
#: bookwyrm/templates/snippets/create_status/content_warning_field.html:10
msgid "Content warning:"
-msgstr ""
+msgstr "Aviso sobre o contido:"
#: bookwyrm/templates/snippets/create_status/content_warning_field.html:18
msgid "Spoilers ahead!"
-msgstr ""
+msgstr "Contén Spoilers!"
#: bookwyrm/templates/snippets/create_status/content_warning_toggle.html:13
msgid "Include spoiler alert"
-msgstr ""
+msgstr "Incluír alerta de spoiler"
#: bookwyrm/templates/snippets/create_status/layout.html:48
#: bookwyrm/templates/snippets/reading_modals/form.html:7
msgid "Comment:"
-msgstr ""
+msgstr "Comentario:"
#: bookwyrm/templates/snippets/create_status/post_options_block.html:8
#: bookwyrm/templates/snippets/privacy-icons.html:15
@@ -3255,60 +3255,60 @@ msgstr ""
#: bookwyrm/templates/snippets/privacy_select.html:20
#: bookwyrm/templates/snippets/privacy_select_no_followers.html:17
msgid "Private"
-msgstr ""
+msgstr "Privado"
#: bookwyrm/templates/snippets/create_status/post_options_block.html:21
msgid "Post"
-msgstr ""
+msgstr "Publicación"
#: bookwyrm/templates/snippets/create_status/quotation.html:17
msgid "Quote:"
-msgstr ""
+msgstr "Cita:"
#: bookwyrm/templates/snippets/create_status/quotation.html:25
#, python-format
msgid "An excerpt from '%(book_title)s'"
-msgstr ""
+msgstr "Un extracto de '%(book_title)s'"
#: bookwyrm/templates/snippets/create_status/quotation.html:32
msgid "Position:"
-msgstr ""
+msgstr "Posición:"
#: bookwyrm/templates/snippets/create_status/quotation.html:45
msgid "On page:"
-msgstr ""
+msgstr "Na páxina:"
#: bookwyrm/templates/snippets/create_status/quotation.html:51
msgid "At percent:"
-msgstr ""
+msgstr "Na porcentaxe:"
#: bookwyrm/templates/snippets/create_status/review.html:25
#, python-format
msgid "Your review of '%(book_title)s'"
-msgstr ""
+msgstr "A túa recensión de '%(book_title)s'"
#: bookwyrm/templates/snippets/create_status/review.html:40
msgid "Review:"
-msgstr ""
+msgstr "Recensión:"
#: bookwyrm/templates/snippets/delete_readthrough_modal.html:4
msgid "Delete these read dates?"
-msgstr ""
+msgstr "Eliminar estas datas de lectura?"
#: bookwyrm/templates/snippets/delete_readthrough_modal.html:7
#, python-format
msgid "You are deleting this readthrough and its %(count)s associated progress updates."
-msgstr ""
+msgstr "Vas eliminar o diario de lectura e as súas %(count)s actualizacións de progreso da lectura."
#: bookwyrm/templates/snippets/fav_button.html:16
#: bookwyrm/templates/snippets/fav_button.html:17
msgid "Like"
-msgstr ""
+msgstr "Gústame"
#: bookwyrm/templates/snippets/fav_button.html:30
#: bookwyrm/templates/snippets/fav_button.html:31
msgid "Un-like"
-msgstr ""
+msgstr "Retirar gústame"
#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:7
msgid "Show filters"
@@ -3391,26 +3391,26 @@ msgstr[1] "valorado %(title)s : %(display_ratin
#, python-format
msgid "Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s"
msgid_plural "Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Recensión de \"%(book_title)s\" (%(display_rating)s estrela): %(review_title)s"
+msgstr[1] "Recensión de \"%(book_title)s\" (%(display_rating)s estrelas): %(review_title)s"
#: bookwyrm/templates/snippets/generated_status/review_pure_name.html:8
#, python-format
msgid "Review of \"%(book_title)s\": %(review_title)s"
-msgstr ""
+msgstr "Recensión de \"%(book_title)s\": %(review_title)s"
#: bookwyrm/templates/snippets/goal_form.html:4
#, python-format
msgid "Set a goal for how many books you'll finish reading in %(year)s, and track your progress throughout the year."
-msgstr ""
+msgstr "Establece un obxectivo de cantos libros queres ler en %(year)s, e controla a túa progresión durante o ano."
#: bookwyrm/templates/snippets/goal_form.html:16
msgid "Reading goal:"
-msgstr ""
+msgstr "Obxectivo de lectura:"
#: bookwyrm/templates/snippets/goal_form.html:21
msgid "books"
-msgstr ""
+msgstr "libros"
#: bookwyrm/templates/snippets/goal_form.html:26
msgid "Goal privacy:"
@@ -3652,51 +3652,51 @@ msgstr "recensionou %(book)s "
#: bookwyrm/templates/snippets/status/headers/to_read.html:7
#, python-format
msgid "%(username)s wants to read %(book)s "
-msgstr ""
+msgstr "%(username)s quere ler %(book)s "
#: bookwyrm/templates/snippets/status/layout.html:24
#: bookwyrm/templates/snippets/status/status_options.html:17
msgid "Delete status"
-msgstr ""
+msgstr "Eliminar estado"
#: bookwyrm/templates/snippets/status/layout.html:56
#: bookwyrm/templates/snippets/status/layout.html:57
msgid "Boost status"
-msgstr ""
+msgstr "Promover estado"
#: bookwyrm/templates/snippets/status/layout.html:60
#: bookwyrm/templates/snippets/status/layout.html:61
msgid "Like status"
-msgstr ""
+msgstr "Gustar estado"
#: bookwyrm/templates/snippets/status/status.html:10
msgid "boosted"
-msgstr ""
+msgstr "promovido"
#: bookwyrm/templates/snippets/status/status_options.html:7
#: bookwyrm/templates/snippets/user_options.html:7
msgid "More options"
-msgstr ""
+msgstr "Máis opcións"
#: bookwyrm/templates/snippets/switch_edition_button.html:5
msgid "Switch to this edition"
-msgstr ""
+msgstr "Cambiar a esta edición"
#: bookwyrm/templates/snippets/table-sort-header.html:6
msgid "Sorted ascending"
-msgstr ""
+msgstr "Orde ascendente"
#: bookwyrm/templates/snippets/table-sort-header.html:10
msgid "Sorted descending"
-msgstr ""
+msgstr "Orde descendente"
#: bookwyrm/templates/snippets/trimmed_text.html:17
msgid "Show more"
-msgstr ""
+msgstr "Mostrar máis"
#: bookwyrm/templates/snippets/trimmed_text.html:35
msgid "Show less"
-msgstr ""
+msgstr "Mostrar menos"
#: bookwyrm/templates/user/books_header.html:10
msgid "Your books"
@@ -3705,166 +3705,166 @@ msgstr "Os teus libros"
#: bookwyrm/templates/user/books_header.html:15
#, python-format
msgid "%(username)s's books"
-msgstr ""
+msgstr "Libros de %(username)s"
#: bookwyrm/templates/user/goal.html:8
#, python-format
msgid "%(year)s Reading Progress"
-msgstr ""
+msgstr "Progresión da lectura en %(year)s"
#: bookwyrm/templates/user/goal.html:12
msgid "Edit Goal"
-msgstr ""
+msgstr "Editar obxectivo"
#: bookwyrm/templates/user/goal.html:28
#, python-format
msgid "%(name)s hasn't set a reading goal for %(year)s."
-msgstr ""
+msgstr "%(name)s non estableceu un obxectivo de lectura para %(year)s."
#: bookwyrm/templates/user/goal.html:40
#, python-format
msgid "Your %(year)s Books"
-msgstr ""
+msgstr "O teus libros de %(year)s"
#: bookwyrm/templates/user/goal.html:42
#, python-format
msgid "%(username)s's %(year)s Books"
-msgstr ""
+msgstr "Libros de %(username)s para %(year)s"
#: bookwyrm/templates/user/groups.html:9
msgid "Your Groups"
-msgstr ""
+msgstr "Os teus grupos"
#: bookwyrm/templates/user/groups.html:11
#, python-format
msgid "Groups: %(username)s"
-msgstr ""
+msgstr "Grupos: %(username)s"
#: bookwyrm/templates/user/groups.html:17
msgid "Create group"
-msgstr ""
+msgstr "Crear grupo"
#: bookwyrm/templates/user/layout.html:19 bookwyrm/templates/user/user.html:10
msgid "User Profile"
-msgstr ""
+msgstr "Perfil da usuaria"
#: bookwyrm/templates/user/layout.html:45
msgid "Follow Requests"
-msgstr ""
+msgstr "Solicitudes de seguimento"
#: bookwyrm/templates/user/layout.html:70
msgid "Reading Goal"
-msgstr ""
+msgstr "Obxectivo de lectura"
#: bookwyrm/templates/user/layout.html:76
msgid "Groups"
-msgstr ""
+msgstr "Grupos"
#: bookwyrm/templates/user/lists.html:11
#, python-format
msgid "Lists: %(username)s"
-msgstr ""
+msgstr "Listas: %(username)s"
#: bookwyrm/templates/user/lists.html:17 bookwyrm/templates/user/lists.html:29
msgid "Create list"
-msgstr ""
+msgstr "Crear lista"
#: bookwyrm/templates/user/relationships/followers.html:12
#, python-format
msgid "%(username)s has no followers"
-msgstr ""
+msgstr "%(username)s non ten seguidoras"
#: bookwyrm/templates/user/relationships/following.html:6
#: bookwyrm/templates/user/relationships/layout.html:15
msgid "Following"
-msgstr ""
+msgstr "Seguindo"
#: bookwyrm/templates/user/relationships/following.html:12
#, python-format
msgid "%(username)s isn't following any users"
-msgstr ""
+msgstr "%(username)s non segue a ninguén"
#: bookwyrm/templates/user/user.html:16
msgid "Edit profile"
-msgstr ""
+msgstr "Editar perfil"
#: bookwyrm/templates/user/user.html:33
#, python-format
msgid "View all %(size)s"
-msgstr ""
+msgstr "Ver tódolos %(size)s"
#: bookwyrm/templates/user/user.html:46
msgid "View all books"
-msgstr ""
+msgstr "Ver tódolos libros"
#: bookwyrm/templates/user/user.html:59
msgid "User Activity"
-msgstr ""
+msgstr "Actividade da usuaria"
#: bookwyrm/templates/user/user.html:63
msgid "RSS feed"
-msgstr ""
+msgstr "Fonte RSS"
#: bookwyrm/templates/user/user.html:74
msgid "No activities yet!"
-msgstr ""
+msgstr "Sen actividade!"
#: bookwyrm/templates/user/user_preview.html:22
#, python-format
msgid "Joined %(date)s"
-msgstr ""
+msgstr "Uniuse en %(date)s"
#: bookwyrm/templates/user/user_preview.html:26
#, python-format
msgid "%(counter)s follower"
msgid_plural "%(counter)s followers"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(counter)s seguidora"
+msgstr[1] "%(counter)s seguidoras"
#: bookwyrm/templates/user/user_preview.html:27
#, python-format
msgid "%(counter)s following"
-msgstr ""
+msgstr "Seguindo a %(counter)s"
#: bookwyrm/templates/user/user_preview.html:34
#, python-format
msgid "%(mutuals_display)s follower you follow"
msgid_plural "%(mutuals_display)s followers you follow"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%(mutuals_display)s seguidora que segues"
+msgstr[1] "%(mutuals_display)s seguidoras que segues"
#: bookwyrm/templates/user/user_preview.html:38
msgid "No followers you follow"
-msgstr ""
+msgstr "Sen seguidoras que ti segues"
#: bookwyrm/templates/widgets/clearable_file_input_with_warning.html:28
msgid "File exceeds maximum size: 10MB"
-msgstr ""
+msgstr "O ficheiro supera o tamaño máximo: 10MB"
#: bookwyrm/templatetags/utilities.py:31
#, python-format
msgid "%(title)s: %(subtitle)s"
-msgstr ""
+msgstr "%(title)s: %(subtitle)s"
#: bookwyrm/views/imports/import_data.py:64
msgid "Not a valid csv file"
-msgstr ""
+msgstr "Non é un ficheiro csv válido"
#: bookwyrm/views/landing/login.py:69
msgid "Username or password are incorrect"
-msgstr ""
+msgstr "O nome de usuaria ou contrasinal non son correctos"
#: bookwyrm/views/landing/password.py:32
msgid "No user with that email address was found."
-msgstr ""
+msgstr "Non atopamos unha usuaria con ese email."
#: bookwyrm/views/landing/password.py:43
#, python-brace-format
msgid "A password reset link was sent to {email}"
-msgstr ""
+msgstr "Enviamos unha ligazón de restablecemento a {email}"
#: bookwyrm/views/rss_feed.py:35
#, python-brace-format
msgid "Status updates from {obj.display_name}"
-msgstr ""
+msgstr "Actualizacións de estados desde {obj.display_name}"
diff --git a/locale/lt_LT/LC_MESSAGES/django.mo b/locale/lt_LT/LC_MESSAGES/django.mo
index 6a72ab36c..728d0e905 100644
Binary files a/locale/lt_LT/LC_MESSAGES/django.mo and b/locale/lt_LT/LC_MESSAGES/django.mo differ
diff --git a/locale/lt_LT/LC_MESSAGES/django.po b/locale/lt_LT/LC_MESSAGES/django.po
index 9928785a6..16110b069 100644
--- a/locale/lt_LT/LC_MESSAGES/django.po
+++ b/locale/lt_LT/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-17 18:03+0000\n"
-"PO-Revision-Date: 2021-11-17 20:02\n"
+"PO-Revision-Date: 2021-11-22 08:50\n"
"Last-Translator: Mouse Reeve \n"
"Language-Team: Lithuanian\n"
"Language: lt\n"
@@ -48,7 +48,7 @@ msgstr "Neribota"
#: bookwyrm/forms.py:338
msgid "List Order"
-msgstr "Sąrašo užsakymas"
+msgstr "Kaip pridėta į sąrašą"
#: bookwyrm/forms.py:339
msgid "Book Title"
@@ -184,7 +184,7 @@ msgstr "Español (Ispanų)"
#: bookwyrm/settings.py:168
msgid "Galego (Galician)"
-msgstr ""
+msgstr "Galego (galisų)"
#: bookwyrm/settings.py:169
msgid "Français (French)"
@@ -192,7 +192,7 @@ msgstr "Français (Prancūzų)"
#: bookwyrm/settings.py:170
msgid "Lietuvių (Lithuanian)"
-msgstr ""
+msgstr "Lietuvių"
#: bookwyrm/settings.py:171
msgid "Português - Brasil (Brazilian Portuguese)"
@@ -1410,11 +1410,11 @@ msgstr "Importavimo būsena"
#: bookwyrm/templates/import/import_status.html:13
#: bookwyrm/templates/import/import_status.html:27
msgid "Retry Status"
-msgstr ""
+msgstr "Pakartojimo būsena"
#: bookwyrm/templates/import/import_status.html:22
msgid "Imports"
-msgstr ""
+msgstr "Importai"
#: bookwyrm/templates/import/import_status.html:39
msgid "Import started:"
@@ -1422,38 +1422,38 @@ msgstr "Importavimas prasidėjo:"
#: bookwyrm/templates/import/import_status.html:48
msgid "In progress"
-msgstr ""
+msgstr "Vykdoma"
#: bookwyrm/templates/import/import_status.html:50
msgid "Refresh"
-msgstr ""
+msgstr "Atnaujinti"
#: bookwyrm/templates/import/import_status.html:71
#, python-format
msgid "%(display_counter)s item needs manual approval."
msgid_plural "%(display_counter)s items need manual approval."
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%(display_counter)s reikia manualaus patvirtinimo."
+msgstr[1] "%(display_counter)s reikia manualaus patvirtinimo."
+msgstr[2] "%(display_counter)s reikia manualaus patvirtinimo."
+msgstr[3] "%(display_counter)s reikia manualaus patvirtinimo."
#: bookwyrm/templates/import/import_status.html:76
#: bookwyrm/templates/import/manual_review.html:8
msgid "Review items"
-msgstr ""
+msgstr "Peržiūrėti elementus"
#: bookwyrm/templates/import/import_status.html:82
#, python-format
msgid "%(display_counter)s item failed to import."
msgid_plural "%(display_counter)s items failed to import."
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%(display_counter)s nepavyko importuoti."
+msgstr[1] "%(display_counter)s nepavyko importuoti."
+msgstr[2] "%(display_counter)s nepavyko importuoti."
+msgstr[3] "%(display_counter)s nepavyko importuoti."
#: bookwyrm/templates/import/import_status.html:88
msgid "View and troubleshoot failed items"
-msgstr ""
+msgstr "Žiūrėkite ir taisykite nepavykusius elementus"
#: bookwyrm/templates/import/import_status.html:100
msgid "Row"
@@ -1477,7 +1477,7 @@ msgstr "Autorius"
#: bookwyrm/templates/import/import_status.html:112
msgid "Shelf"
-msgstr ""
+msgstr "Lentyna"
#: bookwyrm/templates/import/import_status.html:115
#: bookwyrm/templates/import/manual_review.html:13
@@ -1501,11 +1501,11 @@ msgstr "Būsena"
#: bookwyrm/templates/import/import_status.html:130
msgid "Import preview unavailable."
-msgstr ""
+msgstr "Nepavyko įkelti peržiūros."
#: bookwyrm/templates/import/import_status.html:162
msgid "View imported review"
-msgstr ""
+msgstr "Peržiūrėti įkeltą atsiliepimą"
#: bookwyrm/templates/import/import_status.html:176
msgid "Imported"
@@ -1513,28 +1513,28 @@ msgstr "Importuota"
#: bookwyrm/templates/import/import_status.html:182
msgid "Needs manual review"
-msgstr ""
+msgstr "Reikalingas manualus atsiliepimas"
#: bookwyrm/templates/import/import_status.html:195
msgid "Retry"
-msgstr ""
+msgstr "Bandyti dar kartą"
#: bookwyrm/templates/import/import_status.html:213
msgid "This import is in an old format that is no longer supported. If you would like to troubleshoot missing items from this import, click the button below to update the import format."
-msgstr ""
+msgstr "Tai seno formato importas, kuris nebepalaikomas. Jei norite matyti importo metu praleistus elementus, spustelėkite žemiau esantį mygtuką ir atnaujinkite importavimo formatą."
#: bookwyrm/templates/import/import_status.html:215
msgid "Update import"
-msgstr ""
+msgstr "Atnaujinti importą"
#: bookwyrm/templates/import/manual_review.html:5
#: bookwyrm/templates/import/troubleshoot.html:4
msgid "Import Troubleshooting"
-msgstr ""
+msgstr "Importo problemų sprendimas"
#: bookwyrm/templates/import/manual_review.html:21
msgid "Approving a suggestion will permanently add the suggested book to your shelves and associate your reading dates, reviews, and ratings with that book."
-msgstr ""
+msgstr "Jei patvirtinsite siūlymą, siūloma knyga visam laikui bus įkelta į Jūsų lentyną, susieta su skaitymo datomis, atsiliepimais ir knygos reitingais."
#: bookwyrm/templates/import/manual_review.html:58
#: bookwyrm/templates/lists/curate.html:57
@@ -1551,31 +1551,31 @@ msgstr "Galite atsisiųsti savo „Goodreads“ duomenis iš open an issue if you are seeing unexpected failed items."
-msgstr ""
+msgstr "Jei matote netikėtų nesklandumų, susisiekite su administratoriumi arba registruokite problemą ."
#: bookwyrm/templates/landing/about.html:7 bookwyrm/templates/layout.html:230
#, python-format
@@ -3666,7 +3666,7 @@ msgstr "pacitavo %(book)s "
#: bookwyrm/templates/snippets/status/headers/rating.html:3
#, python-format
msgid "rated %(book)s :"
-msgstr "įvertinta %(book)s :"
+msgstr "įvertino %(book)s :"
#: bookwyrm/templates/snippets/status/headers/read.html:7
#, python-format
@@ -3846,7 +3846,7 @@ msgstr "Įrašų dar nėra"
#: bookwyrm/templates/user/user_preview.html:22
#, python-format
msgid "Joined %(date)s"
-msgstr "Joined %(date)s"
+msgstr "Prisijungė %(date)s"
#: bookwyrm/templates/user/user_preview.html:26
#, python-format
diff --git a/locale/zh_Hans/LC_MESSAGES/django.mo b/locale/zh_Hans/LC_MESSAGES/django.mo
index da41e106d..1d1227f80 100644
Binary files a/locale/zh_Hans/LC_MESSAGES/django.mo and b/locale/zh_Hans/LC_MESSAGES/django.mo differ
diff --git a/locale/zh_Hant/LC_MESSAGES/django.mo b/locale/zh_Hant/LC_MESSAGES/django.mo
index f46e61fc0..f9ca27be9 100644
Binary files a/locale/zh_Hant/LC_MESSAGES/django.mo and b/locale/zh_Hant/LC_MESSAGES/django.mo differ
diff --git a/requirements.txt b/requirements.txt
index 2cb1eec97..f308b43b9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
celery==4.4.2
colorthief==0.2.1
Django==3.2.5
-django-imagekit==4.0.2
+django-imagekit==4.1.0
django-model-utils==4.0.0
environs==9.3.4
flower==0.9.4