Compare commits

...

5 Commits

Author SHA1 Message Date
Hosted Weblate 582ed06be4 Translated using Weblate (French)
Currently translated at 100.0% (362 of 362 strings)

Co-authored-by: Banizath <baznitah@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/fr/
Translation: TagStudio/Strings
2026-05-09 04:49:06 +02:00
Hosted Weblate aff0eb823f Translated using Weblate (Russian)
Currently translated at 88.6% (321 of 362 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: werdei <48105614+WerDei@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/tagstudio/strings/ru/
Translation: TagStudio/Strings
2026-05-09 04:49:06 +02:00
Jann Stute b0decac610 refactor: sql migrations (#1295)
* refactor: cleanup parameters of open_library and open_sqlite_library

* doc: notes on what tables are affected by which migration steps

* refactor(migration order): move DBv6 repairs

* refactor(migration order): move DBv8 repairs

* refactor(migration order): move DBv9 repairs

* refactor(migration order): move DBv100 repairs

* refactor(migration order): move DBv102 repairs

* refactor: merge migration methods

* doc: final comment changes

* fix: query tag ids independent of future DB changes
2026-05-08 19:37:09 -07:00
Jann Stute 910d2b735d refactor: cleanup parameters of open_library and open_sqlite_library (#1294) 2026-05-08 19:36:01 -07:00
Travis Abendshien 47d4de5825 chore: bump rawpy version to ~=0.27 2026-05-08 13:25:31 -07:00
8 changed files with 108 additions and 102 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ dependencies = [
"pydub~=0.25",
"PySide6==6.8.0.*",
"rarfile==4.2",
"rawpy~=0.24",
"rawpy~=0.27",
"Send2Trash~=1.8",
"SQLAlchemy~=2.0",
"srctools~=2.6",
+67 -69
View File
@@ -218,7 +218,6 @@ class Library:
"""Class for the Library object, and all CRUD operations made upon it."""
library_dir: Path | None = None
storage_path: Path | str | None = None
engine: Engine | None = None
folder: Folder | None = None
included_files: set[Path] = set()
@@ -233,7 +232,6 @@ class Library:
if self.engine:
self.engine.dispose()
self.library_dir = None
self.storage_path = None
self.folder = None
self.included_files = set()
@@ -349,33 +347,36 @@ class Library:
else:
return tag.name
def open_library(
self, library_dir: Path, storage_path: Path | str | None = None
def open_library(self, library_dir: Path, in_memory: bool = False) -> LibraryStatus:
"""Wrapper for open_sqlite_library.
Handles in-memory storage and checks whether a JSON-migration is necessary.
"""
assert isinstance(library_dir, Path)
if in_memory:
return self.open_sqlite_library(library_dir, is_new=True, storage_path=":memory:")
is_new = True
sql_path = library_dir / TS_FOLDER_NAME / SQL_FILENAME
if self.verify_ts_folder(library_dir) and (is_new := not sql_path.exists()):
json_path = library_dir / TS_FOLDER_NAME / JSON_FILENAME
if json_path.exists():
return LibraryStatus(
success=False,
library_path=library_dir,
message="[JSON] Legacy v9.4 library requires conversion to v9.5+",
json_migration_req=True,
)
return self.open_sqlite_library(library_dir, is_new, str(sql_path))
def open_sqlite_library(
self, library_dir: Path, is_new: bool, storage_path: str
) -> LibraryStatus:
is_new: bool = True
if storage_path == ":memory:":
self.storage_path = storage_path
is_new = True
return self.open_sqlite_library(library_dir, is_new)
else:
self.storage_path = library_dir / TS_FOLDER_NAME / SQL_FILENAME
assert isinstance(self.storage_path, Path)
if self.verify_ts_folder(library_dir) and (is_new := not self.storage_path.exists()):
json_path = library_dir / TS_FOLDER_NAME / JSON_FILENAME
if json_path.exists():
return LibraryStatus(
success=False,
library_path=library_dir,
message="[JSON] Legacy v9.4 library requires conversion to v9.5+",
json_migration_req=True,
)
return self.open_sqlite_library(library_dir, is_new)
def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus:
connection_string = URL.create(
drivername="sqlite",
database=str(self.storage_path),
database=storage_path,
)
# NOTE: File-based databases should use NullPool to create new DB connection in order to
# keep connections on separate threads, which prevents the DB files from being locked
@@ -384,7 +385,7 @@ class Library:
# More info can be found on the SQLAlchemy docs:
# https://docs.sqlalchemy.org/en/20/changelog/migration_07.html
# Under -> sqlite-the-sqlite-dialect-now-uses-nullpool-for-file-based-databases
poolclass = None if self.storage_path == ":memory:" else NullPool
poolclass = None if storage_path == ":memory:" else NullPool
loaded_db_version: int = 0
logger.info(
@@ -422,8 +423,8 @@ class Library:
logger.info(f"[Library] DB_VERSION: {loaded_db_version}")
make_tables(self.engine)
# Add default tag color namespaces.
if is_new:
# Add default tag color namespaces.
namespaces = default_color_groups.namespaces()
try:
session.add_all(namespaces)
@@ -432,8 +433,7 @@ class Library:
logger.error("[Library] Couldn't add default tag color namespaces", error=e)
session.rollback()
# Add default tag colors.
if is_new:
# Add default tag colors.
tag_colors: list[TagColorGroup] = default_color_groups.standard()
tag_colors += default_color_groups.pastels()
tag_colors += default_color_groups.shades()
@@ -448,8 +448,7 @@ class Library:
logger.error("[Library] Couldn't add default tag colors", error=e)
session.rollback()
# Add default tags.
if is_new:
# Add default tags.
tags = get_default_tags()
try:
session.add_all(tags)
@@ -530,35 +529,36 @@ class Library:
# Apply any post-SQL migration patches.
if not is_new:
assert loaded_db_version >= 6
# save backup if patches will be applied
if loaded_db_version < DB_VERSION:
self.library_dir = library_dir
self.save_library_backup_to_disk()
self.library_dir = None
# NOTE: Depending on the data, some data and schema changes need to be applied in
# different orders. This chain of methods can likely be cleaned up and/or moved.
# migrate DB step by step from one version to the next
if loaded_db_version < 7:
# changes: value_type, tags
self.__apply_db7_migration(session)
if loaded_db_version < 8:
self.__apply_db8_schema_changes(session)
# changes: tag_colors
self.__apply_db8_migration(session)
if loaded_db_version < 9:
self.__apply_db9_schema_changes(session)
if loaded_db_version < 103:
self.__apply_db103_schema_changes(session)
if loaded_db_version == 6:
self.__apply_repairs_for_db6(session)
if loaded_db_version >= 6 and loaded_db_version < 8:
self.__apply_db8_default_data(session)
if loaded_db_version < 9:
self.__apply_db9_filename_population(session)
# changes: entries
self.__apply_db9_migration(session)
if loaded_db_version < 100:
self.__apply_db100_parent_repairs(session)
# changes: tag_parents
self.__apply_db100_migration(session)
if loaded_db_version < 102:
self.__apply_db102_repairs(session)
# changes: tag_parents
self.__apply_db102_migration(session)
if loaded_db_version < 103:
self.__apply_db103_default_data(session)
# changes: tags
self.__apply_db103_migration(session)
# Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist
# TODO: do this in the migration step that will remove the preferences table
self.migrate_sql_to_ts_ignore(library_dir)
# Update DB_VERSION
@@ -569,8 +569,8 @@ class Library:
self.library_dir = library_dir
return LibraryStatus(success=True, library_path=library_dir)
def __apply_repairs_for_db6(self, session: Session):
"""Apply database repairs introduced in DB_VERSION 7."""
def __apply_db7_migration(self, session: Session):
"""Migrate DB from DB_VERSION 6 to 7."""
logger.info("[Library][Migration] Applying patches to DB_VERSION: 6 library...")
with session:
# Repair "Description" fields with a TEXT_LINE key instead of a TEXT_BOX key.
@@ -583,7 +583,7 @@ class Library:
session.flush()
# Repair tags that may have a disambiguation_id pointing towards a deleted tag.
all_tag_ids: set[int] = {tag.id for tag in self.tags}
all_tag_ids = session.scalars(text("SELECT DISTINCT id FROM tags")).all()
disam_stmt = (
update(Tag)
.where(Tag.disambiguation_id.not_in(all_tag_ids))
@@ -592,9 +592,8 @@ class Library:
session.execute(disam_stmt)
session.commit()
def __apply_db8_schema_changes(self, session: Session):
"""Apply database schema changes introduced in DB_VERSION 8."""
# TODO: Use Alembic for this part instead
def __apply_db8_migration(self, session: Session):
"""Migrate DB from DB_VERSION 7 to 8."""
# Add the missing color_border column to the TagColorGroups table.
color_border_stmt = text(
"ALTER TABLE tag_colors ADD COLUMN color_border BOOLEAN DEFAULT FALSE NOT NULL"
@@ -610,8 +609,7 @@ class Library:
)
session.rollback()
def __apply_db8_default_data(self, session: Session):
"""Apply default data changes introduced in DB_VERSION 8."""
# collect new default tag colors
tag_colors: list[TagColorGroup] = default_color_groups.standard()
tag_colors += default_color_groups.pastels()
tag_colors += default_color_groups.shades()
@@ -660,8 +658,9 @@ class Library:
)
session.rollback()
def __apply_db9_schema_changes(self, session: Session):
"""Apply database schema changes introduced in DB_VERSION 9."""
def __apply_db9_migration(self, session: Session):
"""Migrate DB from DB_VERSION 8 to 9."""
# Apply database schema changes
add_filename_column = text(
"ALTER TABLE entries ADD COLUMN filename TEXT NOT NULL DEFAULT ''"
)
@@ -676,15 +675,14 @@ class Library:
)
session.rollback()
def __apply_db9_filename_population(self, session: Session):
"""Populate the filename column introduced in DB_VERSION 9."""
# Populate the new filename column.
for entry in self.all_entries():
session.merge(entry).filename = entry.path.name
session.commit()
logger.info("[Library][Migration] Populated filename column in entries table")
def __apply_db100_parent_repairs(self, session: Session):
"""Swap the child_id and parent_id values in the TagParent table."""
def __apply_db100_migration(self, session: Session):
"""Migrate DB to DB_VERSION 100."""
with session:
# Repair parent-child tag relationships that are the wrong way around.
stmt = update(TagParent).values(
@@ -695,17 +693,18 @@ class Library:
session.commit()
logger.info("[Library][Migration] Refactored TagParent table")
def __apply_db102_repairs(self, session: Session):
"""Repair tag_parents rows with references to deleted tags."""
def __apply_db102_migration(self, session: Session):
"""Migrate DB to DB_VERSION 102."""
with session:
all_tag_ids: list[int] = [t.id for t in self.tags]
all_tag_ids = session.scalars(text("SELECT DISTINCT id FROM tags")).all()
stmt = delete(TagParent).where(TagParent.parent_id.not_in(all_tag_ids))
session.execute(stmt)
session.commit()
logger.info("[Library][Migration] Verified TagParent table data")
def __apply_db103_schema_changes(self, session: Session):
"""Apply database schema changes introduced in DB_VERSION 103."""
def __apply_db103_migration(self, session: Session):
"""Migrate DB from DB_VERSION 102 to 103."""
# add the new hidden column for tags
add_is_hidden_column = text(
"ALTER TABLE tags ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT 0"
)
@@ -720,8 +719,7 @@ class Library:
)
session.rollback()
def __apply_db103_default_data(self, session: Session):
"""Apply default data changes introduced in DB_VERSION 103."""
# mark the "Archived" tag as hidden
try:
session.query(Tag).filter(Tag.id == TAG_ARCHIVED).update({"is_hidden": True})
session.commit()
@@ -49,8 +49,8 @@ class PreviewThumb(PreviewThumbView):
stats.width = image.width
stats.height = image.height
except (
rawpy._rawpy._rawpy.LibRawIOError, # pyright: ignore[reportAttributeAccessIssue]
rawpy._rawpy.LibRawFileUnsupportedError, # pyright: ignore[reportAttributeAccessIssue]
rawpy.LibRawIOError,
rawpy.LibRawFileUnsupportedError,
FileNotFoundError,
):
pass
+3 -2
View File
@@ -410,11 +410,12 @@ class JsonMigrationModal(QObject):
self.temp_path: Path = (
self.json_lib.library_dir / TS_FOLDER_NAME / "migration_ts_library.sqlite"
)
self.sql_lib.storage_path = self.temp_path
if self.temp_path.exists():
logger.info('Temporary migration file "temp_path" already exists. Removing...')
self.temp_path.unlink()
self.sql_lib.open_sqlite_library(self.json_lib.library_dir, is_new=True)
self.sql_lib.open_sqlite_library(
self.json_lib.library_dir, is_new=True, storage_path=str(self.temp_path)
)
yield Translations.format(
"json_migration.migrating_files_entries", entries=len(self.json_lib.entries)
)
+2 -2
View File
@@ -1121,8 +1121,8 @@ class ThumbRenderer(QObject):
)
except (
DecompressionBombError,
rawpy._rawpy.LibRawIOError, # pyright: ignore[reportAttributeAccessIssue]
rawpy._rawpy.LibRawFileUnsupportedError, # pyright: ignore[reportAttributeAccessIssue]
rawpy.LibRawIOError,
rawpy.LibRawFileUnsupportedError,
) as e:
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
return im
+21 -21
View File
@@ -1,9 +1,9 @@
{
"about.config_path": "Chemin de Configuration",
"about.description": "TagStudio est une application d'organisation de photos et de fichiers avec un système de tags qui mets en avant la liberté et flexibilité à l'utilisateur. Pas de programmes ou de formats propriétaires, pas la moindre trace de fichiers secondaires, et pas de bouleversement complet de la structure de votre système de fichiers.",
"about.description": "TagStudio est une application d'organisation de photos et de fichiers avec un système de tags qui met en avant la liberté et flexibilité à l'utilisateur. Pas de programmes ou de formats propriétaires, pas la moindre trace de fichiers secondaires, et pas de bouleversement complet de la structure de votre système de fichiers.",
"about.documentation": "Documentation",
"about.license": "Licence",
"about.module.found": "Trouver",
"about.module.found": "Trouvé",
"about.title": "À propos de TagStudio",
"about.website": "Site Internet",
"app.git": "Git Commit",
@@ -12,10 +12,10 @@
"color.color_border": "Utiliser la couleur secondaire sur la bordure",
"color.confirm_delete": "Voulez vous vraiment supprimer la couleur \"{color_name}\"?",
"color.delete": "Supprimer le Tag",
"color.import_pack": "Importer un Pack de Couleur",
"color.import_pack": "Importer un Pack de Couleurs",
"color.name": "Nom",
"color.namespace.delete.prompt": "Voulez-vous vraiment supprimer ce groupe de couleurs? Cela supprimera TOUTES les couleurs du groupe!",
"color.namespace.delete.title": "Supprimer le namespace de couleur",
"color.namespace.delete.prompt": "Voulez-vous vraiment supprimer ce groupe de couleurs ? Cela supprimera TOUTES les couleurs du groupe !",
"color.namespace.delete.title": "Supprimer l'espace de noms de couleurs",
"color.new": "Nouvelle couleur",
"color.placeholder": "Couleur",
"color.primary": "Couleur Primaire",
@@ -33,8 +33,8 @@
"drop_import.progress.window_title": "Importer des Fichiers",
"drop_import.title": "Fichier(s) en Conflit",
"edit.color_manager": "Gérer la Couleur des Tags",
"edit.copy_fields": "Copier les Fields",
"edit.paste_fields": "Coller les Fields",
"edit.copy_fields": "Copier les Champs",
"edit.paste_fields": "Coller les Champs",
"edit.tag_manager": "Gérer les Tags",
"entries.duplicate.merge": "Fusion des entrées dupliquées",
"entries.duplicate.merge.label": "Fusionner les entrées dupliquées...",
@@ -42,19 +42,19 @@
"entries.duplicates.description": "Les entrées dupliquées sont définies comme des entrées multiple qui pointent vers le même fichier sur le disque. Les fusionner va combiner les tags et metadatas de tous les duplicatas vers une seule entrée consolidée. Elles ne doivent pas être confondues avec les \"fichiers en doublon\", qui sont des doublons de vos fichiers en dehors de TagStudio.",
"entries.generic.refresh_alt": "&Recharger",
"entries.generic.remove.removing": "Suppression des Entrées",
"entries.generic.remove.removing_count": "Suppressions de {count} entrées...",
"entries.generic.remove.removing_count": "Suppression de {count} entrées...",
"entries.ignored.description": "Les entrées de fichier sont considérées comme « ignorées » si elles ont été ajoutées à la bibliothèque avant que les règles d'ignorance de l'utilisateur (via le fichier « .ts_ignore ») aient été mises à jour pour les exclure. Les fichiers ignorés sont conservés dans la bibliothèque par défaut afin d'éviter toute perte accidentelle de données lors de la mise à jour des règles d'ignorance.",
"entries.ignored.ignored_count": "Entrées Ignorées : {count}",
"entries.ignored.remove": "Supprimer les entrées ignorées",
"entries.ignored.remove_alt": "Supprim&er les entrées ignorées",
"entries.ignored.scanning": "Recherche des entrées ignorées dans la bibliothèque...",
"entries.ignored.title": "Corriger les entrées ignorées",
"entries.mirror": "&Refléter",
"entries.mirror.confirmation": "Êtes-vous sûr de vouloir répliquer les {count} Entrées suivantes?",
"entries.mirror": "&Répliquer",
"entries.mirror.confirmation": "Êtes-vous sûr de vouloir répliquer les {count} Entrées suivantes ?",
"entries.mirror.label": "Réplication de {idx}/{total} Entrées...",
"entries.mirror.title": "Réplication des Entrées",
"entries.mirror.window_title": "Entrée Miroir",
"entries.remove.plural.confirm": "Êtes-vous sûr de vouloir supprimer les <b>{count}</b> entrées suivantes ? Aucun fichiers sur votre disque ne sera supprimée.",
"entries.mirror.window_title": "Entrées Répliqués",
"entries.remove.plural.confirm": "Êtes-vous sûr de vouloir supprimer les <b>{count}</b> entrées suivantes ? Aucun fichier sur votre disque ne sera supprimée.",
"entries.remove.singular.confirm": "Êtes-vous sûr de vouloir supprimer cette entrée de votre bibliothèque ? Aucun fichier sur le disque ne sera supprimé.",
"entries.running.dialog.new_entries": "Ajout de {total} Nouvelles entrées de fichier...",
"entries.running.dialog.title": "Ajout de Nouvelles entrées de fichier",
@@ -68,7 +68,7 @@
"entries.unlinked.scanning": "Balayage de la Bibliothèque pour trouver des Entrées non Liées...",
"entries.unlinked.search_and_relink": "&Rechercher && Relier",
"entries.unlinked.title": "Réparation des Entrées non Liées",
"entries.unlinked.unlinked_count": "Entrées non Liées : {count}",
"entries.unlinked.unlinked_count": "Entrées non Liées : {count}",
"ffmpeg.missing.description": "FFmpeg et/ou FFprobe nont pas été trouvée. FFmpeg est nécessaire pour la lecture de média et les vignettes.",
"ffmpeg.missing.status": "{ffmpeg} : {ffmpeg_status}<br>{ffprobe} : {ffprobe_status}",
"field.copy": "Copier le Champ",
@@ -83,25 +83,25 @@
"file.duplicates.dupeguru.file_extension": "Fichiers DupeGuru (*.dupeguru)",
"file.duplicates.dupeguru.load_file": "&Charger un Fichier DupeGuru",
"file.duplicates.dupeguru.no_file": "Aucun Fichier DupeGuru Sélectionné",
"file.duplicates.dupeguru.open_file": "Ouvrire les Fichiers de Résultats de DupeGuru",
"file.duplicates.dupeguru.open_file": "Ouvrir les Fichiers de Résultats de DupeGuru",
"file.duplicates.fix": "Réparer les Fichiers en Double",
"file.duplicates.matches": "Dupliquer les Correspondances de Fichier: {count}",
"file.duplicates.matches_uninitialized": "Dupliquer les Correspondances de Fichier : N/A",
"file.duplicates.matches_uninitialized": "Dupliquer les Correspondances de Fichier : N/A",
"file.duplicates.mirror.description": "Repliquer les données d'entrée dans chaque jeu de correspondances en double, en combinant toutes les données sans supprimer ni dupliquer de champs. Cette opération ne supprime aucun fichier ni aucune donnée.",
"file.duplicates.mirror_entries": "&Répliquer les Entrées",
"file.duration": "Durée",
"file.not_found": "Fichier non trouvé",
"file.open_file": "Ouvrir un Fichier",
"file.open_file_with": "Ouvrir le fichier avec",
"file.open_location.generic": "Ouvrir le Fichier dans l'Explorateur de Fichier",
"file.open_location.generic": "Montrer le fichier dans l'Explorateur de Fichier",
"file.open_location.mac": "Montrer dans le Finder",
"file.open_location.windows": "Montrer dans l'explorateur de Fichiers",
"file.path": "Chemin du Fichier",
"folders_to_tags.close_all": "Tout Fermer",
"folders_to_tags.converting": "Conversion des dossiers en Tags",
"folders_to_tags.description": "Créé des Tags basés sur votre arborescence de dossier et les applique à vos entrées.\nLa structure ci-dessous affiche tous les labels qui seront créés et à quelles entrées ils seront appliqués.",
"folders_to_tags.description": "Créé des Tags basés sur votre arborescence de dossier et les applique à vos entrées.\nLa structure ci-dessous affiche tous les Tags qui seront créés et à quelles entrées ils seront appliqués.",
"folders_to_tags.open_all": "Tout Ouvrir",
"folders_to_tags.title": "Créer un Label à partir d'un Dossier",
"folders_to_tags.title": "Créer un Tag à partir d'un Dossier",
"generic.add": "Ajouter",
"generic.apply": "Appliquer",
"generic.apply_alt": "&Appliquer",
@@ -115,7 +115,7 @@
"generic.delete_alt": "&Supprimer",
"generic.done": "Terminé",
"generic.done_alt": "&Terminé",
"generic.edit": "Éditer",
"generic.edit": "Modifier",
"generic.edit_alt": "&Modifier",
"generic.filename": "Nom de fichier",
"generic.missing": "Manquant",
@@ -150,8 +150,8 @@
"ignore.open_file": "Afficher le fichier \"{ts_ignore}\" sur le Disque",
"json_migration.checking_for_parity": "Vérification de la Parité...",
"json_migration.creating_database_tables": "Création des Tables de Base de Données SQL...",
"json_migration.description": "<br>Démarrez et prévisualisez les résultats du processus de migration de la bibliothèque. La bibliothèque convertie <i>ne</i> sera utilisée que si vous cliquez sur \"Terminer la migration\". <br><br>Les données de la bibliothèque doivent soit avoir des valeurs correspondantes, soit comporter un label \"Matched\". Les valeurs qui ne correspondent pas seront affichées en rouge et comporteront un symbole \"<b>(!)</b>\" à côté d'elles.<br><center><i>Ce processus peut prendre jusqu'à plusieurs minutes pour les bibliothèques plus volumineuses.</i></center>",
"json_migration.discrepancies_found": "Divergence Détectées dans la Bibliothèque",
"json_migration.description": "<br>Démarrez et prévisualisez les résultats du processus de migration de la bibliothèque. La bibliothèque convertie ne sera utilisée <i>que</i> si vous cliquez sur \"Terminer la migration\". <br><br>Les données de la bibliothèque doivent soit avoir des valeurs correspondantes, soit comporter un label \"Matched\". Les valeurs qui ne correspondent pas seront affichées en rouge et comporteront un symbole \"<b>(!)</b>\" à côté d'elles.<br><center><i>Ce processus peut prendre jusqu'à plusieurs minutes pour les bibliothèques plus volumineuses.</i></center>",
"json_migration.discrepancies_found": "Divergences Détectées dans la Bibliothèque",
"json_migration.discrepancies_found.description": "Des divergences ont été détectées entre le format d'origine et le format converti de la bibliothèque. Veuillez les examiner et choisir de poursuivre la migration ou de l'annuler.",
"json_migration.finish_migration": "Terminer la Migration",
"json_migration.heading.aliases": "Alias :",
+10 -3
View File
@@ -35,19 +35,25 @@
"edit.color_manager": "Редактировать цвета тегов",
"edit.copy_fields": "Копировать поля",
"edit.paste_fields": "Вставить поля",
"edit.tag_manager": "Управлять тегами",
"edit.tag_manager": "Управление тегами",
"entries.duplicate.merge": "Объединить записи-дубликаты",
"entries.duplicate.merge.label": "Объединение записей-дубликатов...",
"entries.duplicate.refresh": "Обновить записи-дубликаты",
"entries.duplicates.description": "Записи-дубликаты — это несколько записей, которые одновременно привязаны к одному файлу. Объединение таких дубликатов соединит все теги и мета данные из этих записей в одну. Записи-дубликаты не стоит путать с несколькими копиями самого файла, которые могут существовать вне TagStudio.",
"entries.duplicates.description": "Записи-дубликаты — это несколько записей, которые одновременно привязаны к одному файлу. Объединение таких дубликатов соединит все теги и мета данные из этих записей в одну. Записи-дубликаты не стоит путать с копиями самого файла, которые существуют вне TagStudio.",
"entries.generic.refresh_alt": "&Обновить",
"entries.generic.remove.removing": "Удаление записей",
"entries.generic.remove.removing_count": "Удаление {count} записей...",
"entries.ignored.ignored_count": "Проигнорированных записей: {count}",
"entries.ignored.remove": "Удалить игнорируемые записи",
"entries.ignored.scanning": "Поиск игнорируемых записей...",
"entries.ignored.title": "Исправить игнорируемые записи",
"entries.mirror": "&Отзеркалить",
"entries.mirror.confirmation": "Вы уверены, что хотите отзеркалить следующие {count} записей?",
"entries.mirror.label": "Отзеркаливание {idx}/{total} записей...",
"entries.mirror.title": "Отзеркаливание записей",
"entries.mirror.window_title": "Отзеркалить записи",
"entries.remove.plural.confirm": "Вы уверены, что хотите удалить {count} записей?",
"entries.remove.plural.confirm": "Вы уверены, что хотите удалить <b>{count}</b> записей? Файлы на диске не будут удалены.",
"entries.remove.singular.confirm": "Вы уверены, что хотите удалить эту запись? Файл на диске не будет удалён.",
"entries.running.dialog.new_entries": "Добавление {total} новых записей...",
"entries.running.dialog.title": "Добавление новых записей",
"entries.tags": "Теги",
@@ -55,6 +61,7 @@
"entries.unlinked.relink.attempting": "Попытка перепривязать {index}/{unlinked_count} записей, {fixed_count} привязано успешно",
"entries.unlinked.relink.manual": "&Ручная привязка",
"entries.unlinked.relink.title": "Привязка записей",
"entries.unlinked.remove": "Удалить откреплённые записи",
"entries.unlinked.scanning": "Сканирование библиотеки на наличие откреплённых записей...",
"entries.unlinked.search_and_relink": "&Поиск и привязка",
"entries.unlinked.title": "Исправить откреплённые записи",
+2 -2
View File
@@ -33,7 +33,7 @@ def cwd():
def file_mediatypes_library():
lib = Library()
status = lib.open_library(Path(""), ":memory:")
status = lib.open_library(Path(""), in_memory=True)
assert status.success
folder = unwrap(lib.folder)
@@ -84,7 +84,7 @@ def library(request, library_dir: Path): # pyright: ignore
library_path = Path(request.param)
lib = Library()
status = lib.open_library(library_path, ":memory:")
status = lib.open_library(library_path, in_memory=True)
assert status.success
folder = unwrap(lib.folder)