Store book information from incoming bookwyrm notes

merge-requests/30/head
Bob Mottram 2023-12-26 12:16:25 +00:00
parent db4f77e52d
commit 72b1ab9efe
30 changed files with 563 additions and 27 deletions

View File

@ -162,6 +162,7 @@ from maps import get_map_links_from_post_content
from maps import get_location_from_post
from maps import add_tag_map_links
from maps import geocoords_from_map_link
from reading import store_book_events
def cache_svg_images(session, base_dir: str, http_prefix: str,
@ -4759,6 +4760,14 @@ def _inbox_after_initial(server, inbox_start_time,
debug)
inbox_start_time = time.time()
# store any bookwyrm type notes
store_book_events(base_dir,
message_json,
system_language,
languages_understood,
translate, debug,
1000)
if _receive_announce(recent_posts_cache,
session, handle, is_group,
base_dir, http_prefix,

294
reading.py 100644
View File

@ -0,0 +1,294 @@
__filename__ = "reading.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.4.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Core"
import os
from utils import get_content_from_post
from utils import has_object_dict
from utils import remove_id_ending
from utils import get_attributed_to
from utils import load_json
from utils import save_json
from utils import remove_html
def get_book_link_from_content(content: str) -> str:
""" Returns a book link from the given content
"""
if '/book/' not in content or \
'://' not in content or \
'"' not in content:
return None
sections = content.split('/book/')
if '"' not in sections[0] or '"' not in sections[1]:
return None
previous_str = sections[0].split('"')[-1]
if '://' not in previous_str:
return None
next_str = sections[1].split('"')[0]
book_url = previous_str + '/book/' + next_str
return book_url
def get_book_from_post(post_json_object: {}) -> {}:
""" Returns a book details from the given post
"""
if 'tag' not in post_json_object:
return {}
if not isinstance(post_json_object['tag'], list):
return {}
for tag_dict in post_json_object['tag']:
if 'type' not in tag_dict:
continue
if not isinstance(tag_dict['type'], str):
continue
if tag_dict['type'] != 'Edition':
continue
if not tag_dict.get('href'):
continue
if not isinstance(tag_dict['href'], str):
continue
if not tag_dict.get('name'):
continue
if not isinstance(tag_dict['name'], str):
continue
tag_dict['name'] = tag_dict['name'].replace('@', '')
return tag_dict
return {}
def get_reading_status(post_json_object: {},
system_language: str,
languages_understood: [],
translate: {}) -> {}:
"""Returns any reading status from the content of a post
"""
post_obj = post_json_object
if has_object_dict(post_json_object):
post_obj = post_json_object['object']
content = get_content_from_post(post_json_object, system_language,
languages_understood,
"content")
if not content:
return {}
book_url = get_book_link_from_content(content)
if not book_url:
return {}
if not post_obj.get('id'):
return {}
if not isinstance(post_obj['id'], str):
return {}
# get the published date
if not post_obj.get('published'):
return {}
if not isinstance(post_obj['published'], str):
return {}
published = post_obj['published']
if post_obj.get('updated'):
if isinstance(post_obj['updated'], str):
published = post_obj['updated']
if not post_obj.get('attributedTo'):
return {}
actor = get_attributed_to(post_obj['attributedTo'])
if not actor:
return {}
# rating of a book
if post_obj.get('rating'):
rating = post_obj['rating']
if isinstance(rating, (float, int)):
translated_str = 'rated'
if translate.get('rated'):
translated_str = translate['rated']
if translated_str in content or \
'rated' in content:
return {
'id': remove_id_ending(post_obj['id']),
'actor': actor,
'type': 'rated',
'href': book_url,
'rating': rating,
'published': published
}
# get the book details from a post tag
book_dict = get_book_from_post(post_json_object)
if not book_dict:
return {}
# want to read a book
translated_str = 'wants to read'
if translate.get('wants to read'):
translated_str = translate['wants to read']
if translated_str in content or \
'wants to read' in content:
book_dict['id'] = remove_id_ending(post_obj['id'])
book_dict['actor'] = actor
book_dict['type'] = 'want'
book_dict['published'] = published
return book_dict
translated_str = 'finished reading'
if translate.get('finished reading'):
translated_str = translate['finished reading']
if translated_str in content or \
'finished reading' in content:
book_dict['id'] = remove_id_ending(post_obj['id'])
book_dict['actor'] = actor
book_dict['type'] = 'finished'
book_dict['published'] = published
return book_dict
return {}
def _add_book_to_reader(reader_books_json: {}, book_dict: {}) -> None:
"""Updates reader books
"""
book_url = book_dict['href']
book_event_type = book_dict['type']
if not reader_books_json.get(book_url):
reader_books_json[book_url] = {
book_event_type: book_dict
}
return
reader_books_json[book_url][book_event_type] = book_dict
def _add_reader_to_book(book_json: {}, book_dict: {}) -> None:
"""Updates book with a new reader
"""
book_event_type = book_dict['type']
actor = book_dict['actor']
if not book_json.get(actor):
book_json[actor] = {
book_event_type: book_dict
}
if book_dict.get('name'):
book_json['title'] = remove_html(book_dict['name'])
return
book_json[actor][book_event_type] = book_dict
if book_dict.get('name'):
book_json['title'] = remove_html(book_dict['name'])
def store_book_events(base_dir: str,
post_json_object: {},
system_language: str,
languages_understood: [],
translate: {},
debug: bool,
max_recent_books: int) -> bool:
"""Saves book events to file under accounts/reading/books
and accounts/reading/readers
"""
book_dict = get_reading_status(post_json_object,
system_language,
languages_understood,
translate)
if not book_dict:
return False
reading_path = base_dir + '/accounts/reading'
if not os.path.isdir(reading_path):
os.mkdir(reading_path)
books_path = reading_path + '/books'
if not os.path.isdir(books_path):
os.mkdir(books_path)
readers_path = reading_path + '/readers'
if not os.path.isdir(readers_path):
os.mkdir(readers_path)
actor = book_dict['actor']
book_url = remove_id_ending(book_dict['href'])
reader_books_filename = \
readers_path + '/' + actor.replace('/', '#') + '.json'
reader_books_json = {}
if os.path.isfile(reader_books_filename):
reader_books_json = load_json(reader_books_filename)
_add_book_to_reader(reader_books_json, book_dict)
if not save_json(reader_books_json, reader_books_filename):
return False
book_id = book_url.replace('/', '#')
book_filename = books_path + '/' + book_id + '.json'
book_json = {}
if os.path.isfile(book_filename):
book_json = load_json(book_filename)
_add_reader_to_book(book_json, book_dict)
if not save_json(book_json, book_filename):
return False
# prepend to the recent books list
recent_books_filename = base_dir + '/accounts/recent_books.txt'
if os.path.isfile(recent_books_filename):
try:
with open(recent_books_filename, 'r+',
encoding='utf-8') as recent_file:
content = recent_file.read()
if book_id + '\n' not in content:
recent_file.seek(0, 0)
recent_file.write(book_id + '\n' + content)
if debug:
print('DEBUG: recent book added')
except OSError as ex:
print('WARN: Failed to write entry to recent books ' +
recent_books_filename + ' ' + str(ex))
else:
try:
with open(recent_books_filename, 'w+',
encoding='utf-8') as recent_file:
recent_file.write(book_id + '\n')
except OSError:
print('EX: unable to write recent books ' +
recent_books_filename)
# deduplicate and limit the length of the recent books list
if os.path.isfile(recent_books_filename):
# load recent books as a list
recent_lines = []
try:
with open(recent_books_filename, 'r',
encoding='utf-8') as recent_file:
recent_lines = recent_file.read().split('\n')
except OSError as ex:
print('WARN: Failed to read recent books trim ' +
recent_books_filename + ' ' + str(ex))
# deduplicate the list
new_recent_lines = []
for line in recent_lines:
if line not in new_recent_lines:
new_recent_lines.append(line)
if len(new_recent_lines) < len(recent_lines):
recent_lines = new_recent_lines
try:
with open(recent_books_filename, 'w+',
encoding='utf-8') as recent_file:
for line in recent_lines:
recent_file.write(line + '\n')
except OSError:
print('EX: unable to deduplicate recent books ' +
recent_books_filename)
# remove excess lines from the list
if len(recent_lines) > max_recent_books:
try:
with open(recent_books_filename, 'w+',
encoding='utf-8') as recent_file:
for ctr in range(max_recent_books):
recent_file.write(recent_lines[ctr] + '\n')
except OSError:
print('EX: unable to trim recent books ' +
recent_books_filename)
return True

152
tests.py
View File

@ -214,6 +214,9 @@ from webapp_theme_designer import color_contrast
from maps import get_map_links_from_post_content
from maps import geocoords_from_map_link
from followerSync import get_followers_sync_hash
from reading import get_book_link_from_content
from reading import get_book_from_post
from reading import get_reading_status
TEST_SERVER_GROUP_RUNNING = False
@ -8222,6 +8225,154 @@ def _test_dateformat():
assert dtime.tzinfo
def _test_book_link():
print('book_link')
content = 'Not a link'
result = get_book_link_from_content(content)
assert result is None
book_url = 'https://bookwyrm.instance/book/1234567'
content = 'xyz wants to read <a ' + \
'href="' + book_url + '"><i>Title</i></a>'
result = get_book_link_from_content(content)
assert result == book_url
book_url = 'bookwyrm.instance/book/1234567'
content = 'xyz wants to read <a ' + \
'href="' + book_url + '"><i>Title</i></a>'
result = get_book_link_from_content(content)
assert result is None
book_url = 'https://bookwyrm.instance/other/1234567'
content = 'xyz wants to read <a ' + \
'href="' + book_url + '"><i>Title</i></a>'
result = get_book_link_from_content(content)
assert result is None
title = 'Tedious Tome'
image_url = 'https://bookwyrm.instance/images/previews/covers/1234.jpg'
book_url = 'https://bookwyrm.instance/book/56789'
content = 'xyz wants to read <a href="' + book_url + \
'"><i>' + title + '</i></a>'
actor = 'https://bookwyrm.instance/user/xyz'
id_str = actor + '/generatednote/63472854'
published = '2024-01-01T10:30:00.2+00:00'
post_json_object = {
'@context': 'https://www.w3.org/ns/activitystreams',
'attachment': [{'@context': 'https://www.w3.org/ns/activitystreams',
'name': title,
'type': 'Document',
'url': image_url}],
'attributedTo': actor,
'cc': [actor + '/followers'],
'content': content,
'id': id_str,
'published': published,
'sensitive': False,
'tag': [{'href': book_url,
'name': title,
'type': 'Edition'}],
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'type': 'Note'}
languages_understood = []
translate = {}
book_dict = get_book_from_post(post_json_object)
assert book_dict
assert book_dict['name'] == title
assert book_dict['href'] == book_url
result = get_reading_status(post_json_object, 'en',
languages_understood,
translate)
assert result.get('type')
assert result['actor'] == actor
assert result['published'] == published
assert result['type'] == 'want'
assert result['href'] == book_url
assert result['name'] == title
assert result['id'] == id_str
title = 'The Rise of the Meritocracy'
image_url = 'https://bookwyrm.instance/images/previews/covers/6735.jpg'
book_url = 'https://bookwyrm.instance/book/7235'
content = 'abc finished reading <a href="' + book_url + \
'"><i>' + title + '</i></a>'
actor = 'https://bookwyrm.instance/user/abc'
id_str = actor + '/generatednote/366458384'
published = '2024-01-02T11:30:00.2+00:00'
post_json_object = {
'@context': 'https://www.w3.org/ns/activitystreams',
'attachment': [{'@context': 'https://www.w3.org/ns/activitystreams',
'name': title,
'type': 'Document',
'url': image_url}],
'attributedTo': actor,
'cc': [actor + '/followers'],
'content': content,
'id': id_str,
'published': published,
'sensitive': False,
'tag': [{'href': book_url,
'name': title,
'type': 'Edition'}],
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'type': 'Note'}
book_dict = get_book_from_post(post_json_object)
assert book_dict
assert book_dict['name'] == title
assert book_dict['href'] == book_url
result = get_reading_status(post_json_object, 'en',
languages_understood,
translate)
assert result.get('type')
assert result['actor'] == actor
assert result['published'] == published
assert result['type'] == 'finished'
assert result['href'] == book_url
assert result['name'] == title
assert result['id'] == id_str
title = 'Pirate Enlightenment, or the Real Libertalia'
image_url = 'https://bookwyrm.instance/images/previews/covers/5283.jpg'
book_url = 'https://bookwyrm.instance/book/78252'
content = 'rated <a href="' + book_url + \
'"><i>' + title + '</i></a>'
actor = 'https://bookwyrm.instance/user/ghi'
rating = 3.5
id_str = actor + '/generatednote/73467834576'
published = '2024-01-03T12:30:00.2+00:00'
post_json_object = {
'@context': 'https://www.w3.org/ns/activitystreams',
'attachment': [{'@context': 'https://www.w3.org/ns/activitystreams',
'name': title,
'type': 'Document',
'url': image_url}],
'attributedTo': actor,
'cc': [actor + '/followers'],
'content': content,
'rating': rating,
'id': id_str,
'published': published,
'sensitive': False,
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'type': 'Note'}
book_dict = get_book_from_post(post_json_object)
assert not book_dict
result = get_reading_status(post_json_object, 'en',
languages_understood,
translate)
assert result.get('type')
assert result['actor'] == actor
assert result['published'] == published
assert result['type'] == 'rated'
assert result['href'] == book_url
assert result['rating'] == rating
assert result['id'] == id_str
def run_all_tests():
base_dir = os.getcwd()
print('Running tests...')
@ -8239,6 +8390,7 @@ def run_all_tests():
_test_checkbox_names()
_test_thread_functions()
_test_functions()
_test_book_link()
_test_dateformat()
_test_is_right_to_left()
_test_format_mixed_rtl()

View File

@ -642,5 +642,8 @@
"Mutuals": "التعاضد",
"Public replies default to unlisted scope": "الردود العامة افتراضية للنطاق غير المدرج",
"About the author": "عن المؤلف",
"Do not show follows on your profile": "لا تظهر المتابعات في ملفك الشخصي"
"Do not show follows on your profile": "لا تظهر المتابعات في ملفك الشخصي",
"rated": "تصنيف",
"wants to read": "يريد أن يقرأ",
"finished reading": "قراءة الانتهاء"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "পারস্পরিক",
"Public replies default to unlisted scope": "অতালিকাভুক্ত সুযোগে সর্বজনীন উত্তর ডিফল্ট",
"About the author": "লেখক সম্পর্কে",
"Do not show follows on your profile": "আপনার প্রোফাইলে অনুসরণ দেখাবেন না"
"Do not show follows on your profile": "আপনার প্রোফাইলে অনুসরণ দেখাবেন না",
"rated": "রেট করা",
"wants to read": "পড়তে চায়",
"finished reading": "পড়া শেষ"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Mútues",
"Public replies default to unlisted scope": "Les respostes públiques són per defecte a l'abast no llistat",
"About the author": "Sobre lautor",
"Do not show follows on your profile": "No mostris els seguidors al teu perfil"
"Do not show follows on your profile": "No mostris els seguidors al teu perfil",
"rated": "valorat",
"wants to read": "vol llegir",
"finished reading": "acabat de llegir"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Cydfuddiol",
"Public replies default to unlisted scope": "Ymatebion cyhoeddus rhagosodedig i gwmpas heb ei restru",
"About the author": "Am yr awdur",
"Do not show follows on your profile": "Peidiwch â dangos dilyniannau ar eich proffil"
"Do not show follows on your profile": "Peidiwch â dangos dilyniannau ar eich proffil",
"rated": "graddio",
"wants to read": "eisiau darllen",
"finished reading": "gorffen darllen"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Gegenseitigkeitsgesellschaften",
"Public replies default to unlisted scope": "Öffentliche Antworten werden standardmäßig auf den nicht aufgeführten Bereich übertragen",
"About the author": "Über den Autor",
"Do not show follows on your profile": "Zeigen Sie keine Follower in Ihrem Profil an"
"Do not show follows on your profile": "Zeigen Sie keine Follower in Ihrem Profil an",
"rated": "bewertet",
"wants to read": "will lesen",
"finished reading": "fertig gelesen"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Αμοιβαία",
"Public replies default to unlisted scope": "Οι δημόσιες απαντήσεις από προεπιλογή στο μη καταχωρισμένο εύρος",
"About the author": "Σχετικά με τον Συγγραφέα",
"Do not show follows on your profile": "Μην εμφανίζονται οι ακόλουθοι στο προφίλ σας"
"Do not show follows on your profile": "Μην εμφανίζονται οι ακόλουθοι στο προφίλ σας",
"rated": "Βαθμολογήθηκε",
"wants to read": "θέλει να διαβάσει",
"finished reading": "τελείωσε την ανάγνωση"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Mutuals",
"Public replies default to unlisted scope": "Public replies default to unlisted scope",
"About the author": "About the author",
"Do not show follows on your profile": "Do not show follows on your profile"
"Do not show follows on your profile": "Do not show follows on your profile",
"rated": "rated",
"wants to read": "wants to read",
"finished reading": "finished reading"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Mutuales",
"Public replies default to unlisted scope": "Las respuestas públicas tienen por defecto un alcance no listado",
"About the author": "Sobre el Autor",
"Do not show follows on your profile": "No mostrar seguidores en tu perfil"
"Do not show follows on your profile": "No mostrar seguidores en tu perfil",
"rated": "clasificado",
"wants to read": "quiere leer",
"finished reading": "lectura terminada"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "متقابل",
"Public replies default to unlisted scope": "پاسخ‌های عمومی به‌طور پیش‌فرض به محدوده فهرست نشده است",
"About the author": "درباره نویسنده",
"Do not show follows on your profile": "فالوورها را در نمایه خود نشان ندهید"
"Do not show follows on your profile": "فالوورها را در نمایه خود نشان ندهید",
"rated": "دارای رتبه",
"wants to read": "می خواهد بخواند",
"finished reading": "خواندن را تمام کرد"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Mutuelles",
"Public replies default to unlisted scope": "Les réponses publiques ont par défaut une portée non répertoriée",
"About the author": "A propos de l'auteur",
"Do not show follows on your profile": "Ne pas afficher les suivis sur votre profil"
"Do not show follows on your profile": "Ne pas afficher les suivis sur votre profil",
"rated": "noté",
"wants to read": "veut lire",
"finished reading": "fini de lire"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Comhpháirteacha",
"Public replies default to unlisted scope": "Freagraí poiblí réamhshocraithe ar scóip neamhliostaithe",
"About the author": "Faoin tÚdar",
"Do not show follows on your profile": "Ná taispeáin na nithe seo a leanas ar do phróifíl"
"Do not show follows on your profile": "Ná taispeáin na nithe seo a leanas ar do phróifíl",
"rated": "rátáil",
"wants to read": "ag iarraidh a léamh",
"finished reading": "léamh críochnaithe"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "הדדיות",
"Public replies default to unlisted scope": "תשובות ציבוריות כברירת מחדל להיקף לא רשום",
"About the author": "על הסופר",
"Do not show follows on your profile": "אל תראה עוקבים בפרופיל שלך"
"Do not show follows on your profile": "אל תראה עוקבים בפרופיל שלך",
"rated": "מדורג",
"wants to read": "רוצה לקרוא",
"finished reading": "סיים לקרוא"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "पारस्परिक",
"Public replies default to unlisted scope": "सार्वजनिक उत्तर डिफ़ॉल्ट रूप से असूचीबद्ध दायरे में आते हैं",
"About the author": "लेखक के बारे में",
"Do not show follows on your profile": "अपनी प्रोफ़ाइल पर फ़ॉलो न दिखाएं"
"Do not show follows on your profile": "अपनी प्रोफ़ाइल पर फ़ॉलो न दिखाएं",
"rated": "मूल्यांकन",
"wants to read": "पढ़ना चाहता है",
"finished reading": "पढ़ना समाप्त"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Mutui",
"Public replies default to unlisted scope": "Per impostazione predefinita, le risposte pubbliche hanno un ambito non elencato",
"About the author": "Circa l'autore",
"Do not show follows on your profile": "Non mostrare follower sul tuo profilo"
"Do not show follows on your profile": "Non mostrare follower sul tuo profilo",
"rated": "valutato",
"wants to read": "vuole leggere",
"finished reading": "finito di leggere"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "相互作用",
"Public replies default to unlisted scope": "パブリック返信はデフォルトで非公開スコープになります",
"About the author": "著者について",
"Do not show follows on your profile": "プロフィールにフォローを表示しない"
"Do not show follows on your profile": "プロフィールにフォローを表示しない",
"rated": "評価された",
"wants to read": "読みたい",
"finished reading": "読み終わった"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "상호",
"Public replies default to unlisted scope": "공개 답글은 기본적으로 비공개 범위로 설정됩니다.",
"About the author": "저자에 대해",
"Do not show follows on your profile": "프로필에 팔로우를 표시하지 않습니다."
"Do not show follows on your profile": "프로필에 팔로우를 표시하지 않습니다.",
"rated": "평가됨",
"wants to read": "읽고 싶어",
"finished reading": "다 읽었다"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Mutuals",
"Public replies default to unlisted scope": "Bersivên gelemperî ji bo çarçoveyek nelîstekirî xwerû dide",
"About the author": "Di derbarê nivîskarê de",
"Do not show follows on your profile": "Li ser profîla xwe şopandinê nîşan nedin"
"Do not show follows on your profile": "Li ser profîla xwe şopandinê nîşan nedin",
"rated": "nirxandin",
"wants to read": "dixwaze bixwîne",
"finished reading": "xwendina xwe qedand"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Mutualiteiten",
"Public replies default to unlisted scope": "Openbare antwoorden hebben standaard een niet-vermeld bereik",
"About the author": "Over de auteur",
"Do not show follows on your profile": "Laat geen volgers zien op je profiel"
"Do not show follows on your profile": "Laat geen volgers zien op je profiel",
"rated": "beoordeeld",
"wants to read": "wil lezen",
"finished reading": "klaar met lezen"
}

View File

@ -638,5 +638,8 @@
"Mutuals": "Mutuals",
"Public replies default to unlisted scope": "Public replies default to unlisted scope",
"About the author": "About the author",
"Do not show follows on your profile": "Do not show follows on your profile"
"Do not show follows on your profile": "Do not show follows on your profile",
"rated": "rated",
"wants to read": "wants to read",
"finished reading": "finished reading"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Wzajemne relacje",
"Public replies default to unlisted scope": "Odpowiedzi publiczne domyślnie mają zakres niepubliczny",
"About the author": "O autorze",
"Do not show follows on your profile": "Nie pokazuj obserwujących w swoim profilu"
"Do not show follows on your profile": "Nie pokazuj obserwujących w swoim profilu",
"rated": "ocenione",
"wants to read": "chce przeczytać",
"finished reading": "skończyłem czytać"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Mútuas",
"Public replies default to unlisted scope": "As respostas públicas são padronizadas para escopo não listado",
"About the author": "Sobre o autor",
"Do not show follows on your profile": "Não mostre seguidores em seu perfil"
"Do not show follows on your profile": "Não mostre seguidores em seu perfil",
"rated": "avaliada",
"wants to read": "quer ler",
"finished reading": "terminei de ler"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Взаимные отношения",
"Public replies default to unlisted scope": "Публичные ответы по умолчанию имеют скрытую область действия.",
"About the author": "Об авторе",
"Do not show follows on your profile": "Не показывать подписчиков в своем профиле"
"Do not show follows on your profile": "Не показывать подписчиков в своем профиле",
"rated": "рейтинг",
"wants to read": "хочет прочитать",
"finished reading": "закончил читать"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Kuheshimiana",
"Public replies default to unlisted scope": "Majibu ya umma kwa chaguomsingi kwa upeo ambao haujaorodheshwa",
"About the author": "Kuhusu mwandishi",
"Do not show follows on your profile": "Usionyeshe wafuasi kwenye wasifu wako"
"Do not show follows on your profile": "Usionyeshe wafuasi kwenye wasifu wako",
"rated": "imekadiriwa",
"wants to read": "anataka kusoma",
"finished reading": "kumaliza kusoma"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Karşılıklar",
"Public replies default to unlisted scope": "Genel yanıtlar varsayılan olarak liste dışı kapsama alınır",
"About the author": "Yazar hakkında",
"Do not show follows on your profile": "Takip edilenleri profilinizde gösterme"
"Do not show follows on your profile": "Takip edilenleri profilinizde gösterme",
"rated": "oy",
"wants to read": "okumak istiyor",
"finished reading": "okumayı bitirdim"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "Мутуали",
"Public replies default to unlisted scope": "Загальнодоступні відповіді за умовчанням мають приватний обсяг",
"About the author": "Про автора",
"Do not show follows on your profile": "Не показувати підписки у вашому профілі"
"Do not show follows on your profile": "Не показувати підписки у вашому профілі",
"rated": "оцінений",
"wants to read": "хоче читати",
"finished reading": "закінчив читати"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "קעגנצייַטיק",
"Public replies default to unlisted scope": "ציבור ענטפֿערס פעליקייַט צו אַנליסטעד פאַרנעם",
"About the author": "וועגן דעם מחבר",
"Do not show follows on your profile": "דו זאלסט נישט ווייַזן די פאלגענדע אויף דיין פּראָפיל"
"Do not show follows on your profile": "דו זאלסט נישט ווייַזן די פאלגענדע אויף דיין פּראָפיל",
"rated": "רייטאַד",
"wants to read": "וויל לייענען",
"finished reading": "פאַרטיק לייענען"
}

View File

@ -642,5 +642,8 @@
"Mutuals": "互助基金",
"Public replies default to unlisted scope": "公开回复默认为不公开范围",
"About the author": "关于作者",
"Do not show follows on your profile": "不要在您的个人资料上显示关注者"
"Do not show follows on your profile": "不要在您的个人资料上显示关注者",
"rated": "额定",
"wants to read": "想读书",
"finished reading": "读完"
}