diff --git a/.gitignore b/.gitignore index 4b07a21..965c08e 100644 --- a/.gitignore +++ b/.gitignore @@ -53,10 +53,14 @@ coverage.xml tmp/ pytest-tmp/ cover/ +.npm-cache/ # Translations *.mo *.pot +!locale/ +!locale/**/ +!locale/**/*.mo # Django stuff: *.log diff --git a/GUI/app.py b/GUI/app.py index 8ca2227..ea17147 100644 --- a/GUI/app.py +++ b/GUI/app.py @@ -74,6 +74,7 @@ PCFolderDialog, SyncReviewWidget, ) +from infrastructure.i18n import tr as _ from infrastructure.media_folders import ( media_folder_entries_to_settings, media_folder_paths, @@ -163,7 +164,7 @@ def _sync_execute_failure_message(result: Any) -> str | None: errors = list(getattr(result, "errors", []) or []) if not errors: - return "Sync failed before making changes." + return _("Sync failed before making changes.") prioritized = None for desc, msg in errors: @@ -182,26 +183,26 @@ def _sync_execute_failure_message(result: Any) -> str | None: def _library_load_failure_message(mount_path: str, error_msg: str) -> str: error_text = error_msg.strip() or "Unknown error" if not _looks_like_device_access_error(error_text): - return f"iOpenPod could not load this iPod library.\n\n{error_text}" + return _("iOpenPod could not load this iPod library.\n\n{error}").format(error=error_text) mount = mount_path.strip() or "the iPod mount" quoted_mount = shlex.quote(mount) if mount_path.strip() else "" - return ( + return _( "iOpenPod could not read this iPod cleanly.\n\n" - f"Mount path: {mount}\n" - f"System error: {error_text}\n\n" + "Mount path: {mount}\n" + "System error: {error}\n\n" "On Linux, this usually means the iPod mount is not accessible, " "the FAT filesystem is dirty, or the current user does not have " "permission to the mount.\n\n" "Try reconnecting the iPod. If it still fails, try remounting it " "read-write:\n" - f" sudo mount -o remount,rw {quoted_mount}\n\n" + " sudo mount -o remount,rw {quoted_mount}\n\n" "If the filesystem is dirty, unmount it before repairing it:\n" - f" sudo umount {quoted_mount}\n" + " sudo umount {quoted_mount}\n" " sudo fsck.vfat -a /dev/sdXN\n\n" "Replace /dev/sdXN with the iPod partition. Do not run fsck while " "the iPod is mounted." - ) + ).format(mount=mount, error=error_text, quoted_mount=quoted_mount) class MainWindow(QMainWindow): @@ -395,6 +396,7 @@ def _build_ui(self): ) self.settingsPage.closed.connect(self.hideSettings) self.settingsPage.theme_changed.connect(self._on_theme_changed) + self.settingsPage.language_changed.connect(self._on_language_changed) self.settingsPage.artwork_appearance_changed.connect( self._on_artwork_appearance_changed ) @@ -429,22 +431,24 @@ def _build_ui(self): no_device_layout.addStretch(1) - title = QLabel("Select an iPod to continue") + title = QLabel(_("Select an iPod to continue")) title.setAlignment(Qt.AlignmentFlag.AlignCenter) title.setFont(QFont(FONT_FAMILY, Metrics.FONT_XXL, QFont.Weight.DemiBold)) title.setStyleSheet(f"color: {Colors.TEXT_PRIMARY}; background: transparent;") no_device_layout.addWidget(title) subtitle = QLabel( - "No device is currently selected.\n" - "Choose an iPod to access your library and sync tools." + _( + "No device is currently selected.\n" + "Choose an iPod to access your library and sync tools." + ) ) subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter) subtitle.setFont(QFont(FONT_FAMILY, Metrics.FONT_MD)) subtitle.setStyleSheet(f"color: {Colors.TEXT_TERTIARY}; background: transparent;") no_device_layout.addWidget(subtitle) - select_btn = QPushButton("Select Device") + select_btn = QPushButton(_("Select Device")) select_btn.setCursor(Qt.CursorShape.PointingHandCursor) select_btn.setFixedWidth(170) select_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_MD, QFont.Weight.DemiBold)) @@ -475,13 +479,13 @@ def _build_ui(self): loading_layout.setSpacing(12) loading_layout.addStretch(1) - loading_title = QLabel("Loading iPod...") + loading_title = QLabel(_("Loading iPod...")) loading_title.setAlignment(Qt.AlignmentFlag.AlignCenter) loading_title.setFont(QFont(FONT_FAMILY, Metrics.FONT_XXL, QFont.Weight.DemiBold)) loading_title.setStyleSheet(f"color: {Colors.TEXT_PRIMARY}; background: transparent;") loading_layout.addWidget(loading_title) - loading_subtitle = QLabel("Reading library and device settings.") + loading_subtitle = QLabel(_("Reading library and device settings.")) loading_subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter) loading_subtitle.setFont(QFont(FONT_FAMILY, Metrics.FONT_MD)) loading_subtitle.setStyleSheet(f"color: {Colors.TEXT_TERTIARY}; background: transparent;") @@ -579,6 +583,10 @@ def _on_theme_changed(self): if settings_scope == "device" and hasattr(self.settingsPage, "set_settings_scope"): self.settingsPage.set_settings_scope("device") + def _on_language_changed(self): + """Rebuild the entire UI after a live language switch.""" + self._rebuild_themed_ui(restore_page=2) + def _on_artwork_appearance_changed(self): """Refresh visible artwork after UI-only artwork settings change.""" self.musicBrowser.refresh_artwork_appearance() @@ -600,8 +608,8 @@ def selectDevice(self): if selected_ipod is None: QMessageBox.warning( self, - "Invalid iPod Folder", - "The selected folder could not be identified as an iPod.", + _("Invalid iPod Folder"), + _("The selected folder could not be identified as an iPod."), ) return folder = selected_ipod.path or folder @@ -616,11 +624,13 @@ def selectDevice(self): else: QMessageBox.warning( self, - "Invalid iPod Folder", - "The selected folder does not appear to be a valid iPod root.\n\n" - "Expected structure:\n" - " /iPod_Control/iTunes/\n\n" - "Please select the root folder of your iPod." + _("Invalid iPod Folder"), + _( + "The selected folder does not appear to be a valid iPod root.\n\n" + "Expected structure:\n" + " /iPod_Control/iTunes/\n\n" + "Please select the root folder of your iPod." + ) ) def onDeviceChanged(self, path: str): @@ -648,7 +658,7 @@ def onDeviceChanged(self, path: str): access = check_ipod_write_access(path) if not access.writable: logger.error("Selected iPod is not writable: %s", access.message) - QMessageBox.critical(self, "iPod Not Writable", access.message) + QMessageBox.critical(self, _("iPod Not Writable"), access.message) if same_device_path(path, self.device_manager.device_path): self.device_manager.device_path = None return @@ -733,9 +743,9 @@ def onDataReady(self): device_name = ( MainWindow._device_name_from_playlists(playlists) or (device_identity.ipod_name if device_identity else "") - or "Unk iPod" + or _("Unknown iPod") ) - model = device_identity.display_name if device_identity else "Unk iPod" + model = device_identity.display_name if device_identity else _("Unknown iPod") db_version_hex = db_data.get('VersionHex', '') if db_data else '' db_version_name = get_version_name(db_version_hex) if db_version_hex else '' @@ -768,7 +778,7 @@ def onDataLoadFailed(self, error_msg: str): logger.error("iPod library load failed: %s", error_msg) QMessageBox.critical( self, - "Could Not Load iPod", + _("Could Not Load iPod"), _library_load_failure_message(device_path, error_msg), ) @@ -777,8 +787,8 @@ def _onIpodTagFixesRequested(self) -> None: if not cache.is_ready(): QMessageBox.information( self, - "Normalize iPod Tags", - "Load an iPod library before running tag normalization.", + _("Normalize iPod Tags"), + _("Load an iPod library before running tag normalization."), ) return @@ -786,8 +796,8 @@ def _onIpodTagFixesRequested(self) -> None: if not tracks: QMessageBox.information( self, - "Normalize iPod Tags", - "No tracks were found in this iPod library.", + _("Normalize iPod Tags"), + _("No tracks were found in this iPod library."), ) return @@ -810,8 +820,8 @@ def _onIpodTagFixesRequested(self) -> None: if not suggestion.changes_by_track: QMessageBox.information( self, - "Normalize iPod Tags", - "No iPod-specific metadata fixes were found for this library.", + _("Normalize iPod Tags"), + _("No iPod-specific metadata fixes were found for this library."), ) return @@ -942,17 +952,18 @@ def _onDeviceRenamed(self, new_name: str): def _onRenameDone(self, result): """Device rename write completed.""" if not result.success: - self._onRenameFailed(result.error or "Database write failed.") + self._onRenameFailed(result.error or _("Database write failed.")) return logger.info("iPod renamed successfully") - Notifier.get_instance().notify("iPod Renamed", "Device name updated successfully") + Notifier.get_instance().notify(_("iPod Renamed"), _("Device name updated successfully")) def _onRenameFailed(self, error_msg: str): """Device rename write failed.""" logger.error("iPod rename failed: %s", error_msg) QMessageBox.critical( - self, "Rename Failed", - f"Failed to rename iPod:\n{error_msg}" + self, + _("Rename Failed"), + _("Failed to rename iPod:\n{error}").format(error=error_msg), ) # ── Eject ────────────────────────────────────────────────────────── @@ -965,9 +976,11 @@ def _flush_quick_writes_for_eject(self) -> bool: if not ok: QMessageBox.warning( self, - "Save In Progress", - f"iOpenPod is still saving {label} to the iPod. " - "Try ejecting again when the save finishes.", + _("Save In Progress"), + _( + "iOpenPod is still saving {label} to the iPod. " + "Try ejecting again when the save finishes." + ).format(label=label), ) return False @@ -992,9 +1005,11 @@ def _settle_background_device_reads_for_eject(self) -> bool: if not pool.waitForDone(5000): QMessageBox.warning( self, - "Still Reading iPod", - "iOpenPod is still finishing background reads from the iPod. " - "Try ejecting again in a moment.", + _("Still Reading iPod"), + _( + "iOpenPod is still finishing background reads from the iPod. " + "Try ejecting again in a moment." + ), ) return False @@ -1010,8 +1025,9 @@ def _onEjectDevice(self): if self._is_sync_running(): QMessageBox.warning( - self, "Sync In Progress", - "Please wait for the current sync to finish before ejecting." + self, + _("Sync In Progress"), + _("Please wait for the current sync to finish before ejecting."), ) return @@ -1031,7 +1047,7 @@ def _onEjectDone(self, message: str): if self._eject_worker is not None: self._eject_worker.deleteLater() self._eject_worker = None - Notifier.get_instance().notify("iPod Ejected", message) + Notifier.get_instance().notify(_("iPod Ejected"), message) self.device_manager.device_path = None # Forget the restored device so it doesn't auto-reconnect next launch. try: @@ -1055,8 +1071,9 @@ def _onEjectFailed(self, error_msg: str): except Exception: logger.debug("Failed to restore UI after eject failure", exc_info=True) QMessageBox.critical( - self, "Eject Failed", - f"Failed to eject the iPod:\n{error_msg}" + self, + _("Eject Failed"), + _("Failed to eject the iPod:\n{error}").format(error=error_msg), ) def _is_sync_running(self) -> bool: @@ -1080,9 +1097,12 @@ def _is_sync_running(self) -> bool: def _on_quick_meta_failed(self, error_msg: str): QMessageBox.warning( - self, "Save Failed", - f"Could not save quick changes to iPod:\n{error_msg}\n\n" - "iOpenPod is reloading the device view from the iPod." + self, + _("Save Failed"), + _( + "Could not save quick changes to iPod:\n{error}\n\n" + "iOpenPod is reloading the device view from the iPod." + ).format(error=error_msg), ) def _create_back_sync_artwork_provider(self, ipod_path: str): @@ -1148,21 +1168,21 @@ def startPCSync(self): self._quick_write_controller.prepare_for_full_sync() ) if not quick_ready: - label = blocked_label or "quick changes" + label = blocked_label or _("quick changes") QMessageBox.warning( self, - "Quick Changes Still Saving", - ( + _("Quick Changes Still Saving"), + _( "iOpenPod is still saving pending quick changes. " - f"Please wait for {label} to finish before starting a full sync." - ), + "Please wait for {label} to finish before starting a full sync." + ).format(label=label), ) return if self.library_cache.is_loading(): QMessageBox.information( self, - "Library Loading", - "Please wait for the iPod library to finish loading.", + _("Library Loading"), + _("Please wait for the iPod library to finish loading."), ) return @@ -1355,8 +1375,8 @@ def _on_tools_download_failed(self, error_msg: str): self._dl_progress = None QMessageBox.critical( self, - "Download Failed", - f"Could not download sync tools:\n\n{error_msg}", + _("Download Failed"), + _("Could not download sync tools:\n\n{error}").format(error=error_msg), ) def _onPodcastSyncRequested(self, plan): @@ -1369,8 +1389,8 @@ def _onPodcastSyncRequested(self, plan): if caps is not None and not caps.supports_podcast and getattr(plan, "to_add", None): QMessageBox.warning( self, - "Unsupported iPod", - "This iPod does not support podcasts.", + _("Unsupported iPod"), + _("This iPod does not support podcasts."), ) return self._plan = plan @@ -1388,19 +1408,19 @@ def _onAlbumConversionRequested(self, album_items: list[dict]) -> None: if self._is_sync_running(): QMessageBox.information( self, - "Sync Running", - "Please wait for the current sync to finish before converting an album.", + _("Sync Running"), + _("Please wait for the current sync to finish before converting an album."), ) return device_manager = self.device_manager if not device_manager.device_path: - QMessageBox.warning(self, "No Device", "Please select an iPod device first.") + QMessageBox.warning(self, _("No Device"), _("Please select an iPod device first.")) return cache = self.library_cache if not cache.is_ready(): - QMessageBox.information(self, "Library Loading", "Please wait for the iPod library to finish loading.") + QMessageBox.information(self, _("Library Loading"), _("Please wait for the iPod library to finish loading.")) return album_item = dict(album_items[0]) @@ -1410,14 +1430,14 @@ def _onAlbumConversionRequested(self, album_items: list[dict]) -> None: album_tracks = resolve_album_tracks(album_item, cache.get_tracks()) except Exception as exc: logger.debug("Album track resolution failed", exc_info=True) - QMessageBox.warning(self, "Album Conversion", str(exc)) + QMessageBox.warning(self, _("Album Conversion"), str(exc)) return if len(album_tracks) < 2: QMessageBox.information( self, - "Album Conversion", - "Choose an album with at least two tracks.", + _("Album Conversion"), + _("Choose an album with at least two tracks."), ) return @@ -1545,22 +1565,22 @@ def _onChapterSplitRequested(self, tracks: list[dict]) -> None: if self._is_sync_running(): QMessageBox.information( self, - "Sync Running", - "Please wait for the current sync to finish before splitting chapters.", + _("Sync Running"), + _("Please wait for the current sync to finish before splitting chapters."), ) return device_manager = self.device_manager if not device_manager.device_path: - QMessageBox.warning(self, "No Device", "Please select an iPod device first.") + QMessageBox.warning(self, _("No Device"), _("Please select an iPod device first.")) return cache = self.library_cache if not cache.is_ready(): QMessageBox.information( self, - "Library Loading", - "Please wait for the iPod library to finish loading.", + _("Library Loading"), + _("Please wait for the iPod library to finish loading."), ) return @@ -1571,7 +1591,7 @@ def _onChapterSplitRequested(self, tracks: list[dict]) -> None: segments = build_chapter_split_segments(track) except Exception as exc: logger.debug("Chapter split validation failed", exc_info=True) - QMessageBox.warning(self, "Chapter Split", str(exc)) + QMessageBox.warning(self, _("Chapter Split"), str(exc)) return settings = self.settings_service.get_effective_settings() @@ -2064,7 +2084,7 @@ def executeSyncPlan(self, selected_items): # Get device path device_manager = self.device_manager if not device_manager.device_path: - QMessageBox.warning(self, "No Device", "No iPod device selected.") + QMessageBox.warning(self, _("No Device"), _("No iPod device selected.")) return original_plan = self._plan # stored in _onSyncDiffComplete @@ -2143,25 +2163,30 @@ def _confirm_sync_until_full_if_needed(self, plan: Any, ipod_path: str) -> bool shortage = max(0, required - disk.free) reserve_label = format_size(SYNC_UNTIL_FULL_RESERVE_BYTES) or "1 MB" - message = ( + message = _( "This sync is estimated to need more space than is available on " "the iPod.\n\n" - f"Available: {format_size(disk.free) or '0 B'}\n" - f"Estimated needed: {format_size(required) or '0 B'}\n" - f"Estimated shortfall: {format_size(shortage) or '0 B'}\n\n" + "Available: {available}\n" + "Estimated needed: {required}\n" + "Estimated shortfall: {shortage}\n\n" "Sync Until Full will copy files in order until the next file would " - f"leave less than {reserve_label} free, then save the database with " + "leave less than {reserve} free, then save the database with " "the items that actually synced." + ).format( + available=format_size(disk.free) or "0 B", + required=format_size(required) or "0 B", + shortage=format_size(shortage) or "0 B", + reserve=reserve_label, ) dialog = QMessageBox(self) dialog.setIcon(QMessageBox.Icon.Warning) - dialog.setWindowTitle("Not Enough Space") - dialog.setText("The selected sync is larger than the iPod's free space.") + dialog.setWindowTitle(_("Not Enough Space")) + dialog.setText(_("The selected sync is larger than the iPod's free space.")) dialog.setInformativeText(message) - cancel_btn = dialog.addButton("Cancel", QMessageBox.ButtonRole.RejectRole) + cancel_btn = dialog.addButton(_("Cancel"), QMessageBox.ButtonRole.RejectRole) sync_btn = dialog.addButton( - "Sync Until Full", + _("Sync Until Full"), QMessageBox.ButtonRole.AcceptRole, ) dialog.setDefaultButton(cancel_btn) @@ -2176,7 +2201,7 @@ def _onSyncExecuteComplete(self, result): self._keep_sync_results_visible_after_rescan = True failure_message = _sync_execute_failure_message(result) if failure_message: - QMessageBox.critical(self, "Sync Failed", failure_message) + QMessageBox.critical(self, _("Sync Failed"), failure_message) # Desktop notification if app is not focused if not self.isActiveWindow(): @@ -2252,7 +2277,7 @@ def _onSyncExecuteError(self, error_msg: str): "You can restore it from the Backups page." ) - QMessageBox.critical(self, "Sync Error", msg) + QMessageBox.critical(self, _("Sync Error"), msg) self.hideSyncReview() def _onConfirmPartialSave(self, n_added: int, n_skipped: int) -> None: @@ -2262,27 +2287,34 @@ def _onConfirmPartialSave(self, n_added: int, n_skipped: int) -> None: if worker is None: return - tracks_word = "track" if n_added == 1 else "tracks" + tracks_word = _("track") if n_added == 1 else _("tracks") skipped_line = ( - f"{n_skipped} more {'track was' if n_skipped == 1 else 'tracks were'} not copied." + ( + _("{count} more track was not copied.") + if n_skipped == 1 + else _("{count} more tracks were not copied.") + ).format(count=n_skipped) if n_skipped > 0 else "" ) msg = QMessageBox(self) - msg.setWindowTitle("Save Partial Sync?") + msg.setWindowTitle(_("Save Partial Sync?")) msg.setIcon(QMessageBox.Icon.Question) msg.setText( - f"{n_added} {tracks_word} were successfully copied to your iPod before the sync was cancelled." + _( + "{count} {tracks_word} were successfully copied to your iPod " + "before the sync was cancelled." + ).format(count=n_added, tracks_word=tracks_word) ) - detail = "Would you like to save these tracks to your iPod's database?" + detail = _("Would you like to save these tracks to your iPod's database?") if skipped_line: detail = skipped_line + "\n\n" + detail - detail += ( + detail += _( "\n\nIf you discard, the copied files will be cleaned up automatically the next time you sync." ) msg.setInformativeText(detail) - save_btn = msg.addButton("Save Partial Database", QMessageBox.ButtonRole.AcceptRole) - discard_btn = msg.addButton("Discard", QMessageBox.ButtonRole.RejectRole) + save_btn = msg.addButton(_("Save Partial Database"), QMessageBox.ButtonRole.AcceptRole) + discard_btn = msg.addButton(_("Discard"), QMessageBox.ButtonRole.RejectRole) msg.setDefaultButton(save_btn) msg.exec() @@ -2386,7 +2418,7 @@ def _on_files_dropped(self, paths: list[Path]): # Switch to sync review and show loading self.centralStack.setCurrentIndex(1) self.syncReview.show_loading() - self.syncReview.loading_label.setText("Reading dropped files...") + self.syncReview.loading_label.setText(_("Reading dropped files...")) settings = self.settings_service.get_effective_settings() device_manager = self.device_manager @@ -2490,7 +2522,7 @@ def __init__( detail_lines: str = "", ): super().__init__(parent) - self.setWindowTitle("Missing Tools") + self.setWindowTitle(_("Missing Tools")) self.setFixedWidth(420) _apply_dialog_background(self) @@ -2509,7 +2541,7 @@ def __init__( icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(icon_label) - title = QLabel(f"{tool_list} Not Found") + title = QLabel(_("{tools} Not Found").format(tools=tool_list)) title.setFont(QFont(FONT_FAMILY, Metrics.FONT_TITLE, QFont.Weight.Bold)) title.setStyleSheet(_label_css(Colors.TEXT_PRIMARY)) title.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -2520,8 +2552,10 @@ def __init__( if can_download: body = QLabel( - "iOpenPod can download these automatically (~80 MB).\n" - "Download now?" + _( + "iOpenPod can download these automatically (~80 MB).\n" + "Download now?" + ) ) else: body = QLabel(detail_lines) @@ -2538,7 +2572,7 @@ def __init__( btn_row.setSpacing(12) if can_download: - no_btn = QPushButton("Not Now") + no_btn = QPushButton(_("Not Now")) no_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) no_btn.setCursor(Qt.CursorShape.PointingHandCursor) no_btn.setMinimumHeight(40) @@ -2552,7 +2586,7 @@ def __init__( no_btn.clicked.connect(self.reject) btn_row.addWidget(no_btn) - yes_btn = QPushButton("Download") + yes_btn = QPushButton(_("Download")) yes_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) yes_btn.setCursor(Qt.CursorShape.PointingHandCursor) yes_btn.setMinimumHeight(40) @@ -2566,7 +2600,7 @@ def __init__( yes_btn.clicked.connect(self.accept) btn_row.addWidget(yes_btn) else: - ok_btn = QPushButton("OK") + ok_btn = QPushButton(_("OK")) ok_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) ok_btn.setCursor(Qt.CursorShape.PointingHandCursor) ok_btn.setMinimumHeight(40) @@ -2588,7 +2622,7 @@ class _DownloadProgressDialog(QDialog): def __init__(self, parent: QWidget): super().__init__(parent) - self.setWindowTitle("Downloading") + self.setWindowTitle(_("Downloading")) self.setFixedSize((380), (180)) self.setModal(True) self.setWindowFlags( @@ -2601,13 +2635,13 @@ def __init__(self, parent: QWidget): layout.setContentsMargins((28), (24), (28), (24)) layout.setSpacing(14) - title = QLabel("Downloading Tools…") + title = QLabel(_("Downloading Tools…")) title.setFont(QFont(FONT_FAMILY, Metrics.FONT_XXL, QFont.Weight.Bold)) title.setStyleSheet(_label_css(Colors.TEXT_PRIMARY)) title.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(title) - self._status = QLabel("Preparing download…") + self._status = QLabel(_("Preparing download…")) self._status.setFont(QFont(FONT_FAMILY, Metrics.FONT_MD)) self._status.setStyleSheet(_label_css(Colors.TEXT_SECONDARY)) self._status.setAlignment(Qt.AlignmentFlag.AlignCenter) diff --git a/GUI/widgets/MBListView.py b/GUI/widgets/MBListView.py index 4b65963..bc24aa5 100644 --- a/GUI/widgets/MBListView.py +++ b/GUI/widgets/MBListView.py @@ -35,6 +35,7 @@ ) from app_core.runtime import display_playlists_from_rows +from infrastructure.i18n import tr as _ from iTunesDB_Shared.constants import ( MEDIA_TYPE_AUDIO, MEDIA_TYPE_AUDIO_VIDEO, @@ -71,6 +72,14 @@ _VOLUME_ZERO_MAGNET_THRESHOLD = 12 +_COUNT_TEMPLATES = { + "video": ("{count:,} video", "{count:,} videos", "{shown:,} of {total:,} video", "{shown:,} of {total:,} videos"), + "episode": ("{count:,} episode", "{count:,} episodes", "{shown:,} of {total:,} episode", "{shown:,} of {total:,} episodes"), + "audiobook": ("{count:,} audiobook", "{count:,} audiobooks", "{shown:,} of {total:,} audiobook", "{shown:,} of {total:,} audiobooks"), + "song": ("{count:,} song", "{count:,} songs", "{shown:,} of {total:,} song", "{shown:,} of {total:,} songs"), + "track": ("{count:,} track", "{count:,} tracks", "{shown:,} of {total:,} track", "{shown:,} of {total:,} tracks"), +} + # ============================================================================= # Formatters - Shared formatters + local display-specific ones @@ -141,9 +150,9 @@ def format_volume(vol: int) -> str: def format_explicit(flag: int) -> str: """Format explicit/clean flag (0=none, 1=explicit, 2=clean).""" if flag == 1: - return "Explicit" + return _("Explicit") if flag == 2: - return "Clean" + return _("Clean") return "" @@ -156,12 +165,12 @@ def format_checked(val: int) -> str: def format_bool_flag(val: int) -> str: """Format a 0/1 flag as Yes/empty.""" - return "Yes" if val else "" + return _("Yes") if val else "" def format_compilation(val: int) -> str: """Format compilation flag.""" - return "Yes" if val else "" + return _("Yes") if val else "" def format_sound_check(val: int) -> str: @@ -202,8 +211,9 @@ def format_chapter_count(val: int) -> str: """Format a chapter count for table display.""" if not val: return "" - noun = "chapter" if val == 1 else "chapters" - return f"{val} {noun}" + if val == 1: + return _("{count} chapter").format(count=val) + return _("{count} chapters").format(count=val) def _chapter_entries(chapter_data: object) -> list[dict]: @@ -634,6 +644,7 @@ def _is_display_merged_playlist(playlist: dict | None) -> bool: DEFAULT_COLUMN_MIN_WIDTH = 48 DEFAULT_COLUMN_MAX_WIDTH = 640 DEFAULT_COLUMN_WIDTH_SAMPLE_LIMIT = 2_000 +DEFAULT_COLUMN_VIEWPORT_MARGIN = 2 def _art_column_width() -> int: @@ -713,7 +724,8 @@ def __init__(self, tracks: list[dict]) -> None: inner.setContentsMargins(14, 10, 14, 10) inner.setSpacing(3) - self._header = QLabel(f"Preparing {n} file{'s' if n != 1 else ''}…") + prep_template = "Preparing {count} file…" if n == 1 else "Preparing {count} files…" + self._header = QLabel(_(prep_template).format(count=n)) self._header.setFont(QFont(FONT_FAMILY, 9, QFont.Weight.Bold)) inner.addWidget(self._header) @@ -759,7 +771,7 @@ def mark_done(self, idx: int) -> None: lbl.setStyleSheet(f"color: {Colors.ACCENT_LIGHT}; background: transparent;") self._done += 1 if self._done == len(self._rows): - self._header.setText("Starting drag…") + self._header.setText(_("Starting drag…")) self._header.setStyleSheet( f"color: {Colors.ACCENT_LIGHT}; background: transparent;" ) @@ -965,6 +977,11 @@ def __init__( self._width_resize_debounce_timer.timeout.connect( self._on_width_resize_debounce_timeout ) + self._default_column_fit_timer = QTimer(self) + self._default_column_fit_timer.setSingleShot(True) + self._default_column_fit_timer.timeout.connect( + self._fit_current_default_columns_to_viewport + ) # Middle-mouse grab-scroll state self._grab_scrolling = False @@ -1174,7 +1191,7 @@ def _setup_table(self) -> None: if header: header.setSectionsMovable(True) header.setSectionResizeMode(QHeaderView.ResizeMode.Interactive) - header.setStretchLastSection(True) + header.setStretchLastSection(False) header.setDefaultSectionSize(150) header.setMinimumSectionSize(40) header.sectionMoved.connect(self._on_header_section_moved) @@ -1441,6 +1458,17 @@ def _queue_column_layout_save(self) -> None: self._column_layout_dirty = True self._column_layout_save_timer.start(COLUMN_LAYOUT_SAVE_DELAY_MS) + def _has_user_column_layout(self) -> bool: + """Return True when column widths/order should be treated as user-owned.""" + content_key = self._active_column_content_key or self._content_type_key() + return bool( + content_key + and self._column_layouts.get(content_key) + or self._column_layout_dirty + or self._width_resize_debounce_timer.isActive() + or self._column_layout_save_timer.isActive() + ) + def _current_column_header_signature( self, ) -> tuple[tuple[str, ...], tuple[tuple[str, int], ...]]: @@ -1543,6 +1571,8 @@ def _on_header_section_resized( """Debounce user-driven header width changes (prevents spam during drag).""" if self._applying_column_layout or old_size == new_size: return + if self._header_interaction_signature is None: + return # Restart debounce timer to batch resize events while dragging self._width_resize_debounce_timer.start(50) # 50ms to catch rapid resizes @@ -1861,6 +1891,115 @@ def _smart_default_column_width(self, logical_col: int) -> int: min(DEFAULT_COLUMN_MAX_WIDTH, math.ceil(desired_width)), ) + @staticmethod + def _fit_widths_to_budget(widths: list[int], budget: int) -> list[int]: + """Shrink default widths proportionally so their sum fits *budget*.""" + if not widths or budget <= 0: + return widths + current_total = sum(widths) + if current_total <= budget: + return widths + + min_total = DEFAULT_COLUMN_MIN_WIDTH * len(widths) + if budget <= min_total: + return [DEFAULT_COLUMN_MIN_WIDTH for _ in widths] + + extra_budget = budget - min_total + extras = [max(0, width - DEFAULT_COLUMN_MIN_WIDTH) for width in widths] + extra_total = sum(extras) + if extra_total <= 0: + return widths + + fitted: list[int] = [] + remainders: list[tuple[float, int]] = [] + for index, extra in enumerate(extras): + scaled = extra * (extra_budget / extra_total) + whole = math.floor(scaled) + fitted.append(DEFAULT_COLUMN_MIN_WIDTH + whole) + remainders.append((scaled - whole, index)) + + remainder = budget - sum(fitted) + for _fraction, index in sorted(remainders, reverse=True): + if remainder <= 0: + break + if fitted[index] < widths[index]: + fitted[index] += 1 + remainder -= 1 + return fitted + + def _default_column_width_budget(self, start_col: int) -> int: + """Return the visible width available to default data columns.""" + viewport = self.table.viewport() + viewport_width = viewport.width() if viewport is not None else self.table.width() + if viewport_width <= 0: + return 0 + + fixed_width = 0 + header = self.table.horizontalHeader() + for logical_col in range(start_col): + fixed_width += ( + header.sectionSize(logical_col) + if header is not None + else self.table.columnWidth(logical_col) + ) + return max( + 0, + viewport_width - fixed_width - DEFAULT_COLUMN_VIEWPORT_MARGIN, + ) + + def _fit_default_widths_to_viewport( + self, + logical_cols: list[int], + widths: list[int], + ) -> list[int]: + """Fit freshly calculated default widths to the current table viewport.""" + if not logical_cols or self._has_user_column_layout(): + return widths + budget = self._default_column_width_budget(min(logical_cols)) + return self._fit_widths_to_budget(widths, budget) + + def _fit_current_default_columns_to_viewport(self) -> None: + """Keep the default, non-user column layout inside the current viewport.""" + if self._applying_column_layout or self._has_user_column_layout(): + return + + header = self.table.horizontalHeader() + if header is None or self.table.columnCount() <= 0: + return + + start_col = 1 if self._show_art else 0 + logical_cols = [ + logical_col + for logical_col in range(start_col, self.table.columnCount()) + if self._col_key_for_logical(logical_col) is not None + ] + if not logical_cols: + return + + current_widths = [header.sectionSize(logical_col) for logical_col in logical_cols] + natural_widths = [ + self._smart_default_column_width(logical_col) + for logical_col in logical_cols + ] + fitted = self._fit_default_widths_to_viewport(logical_cols, natural_widths) + if fitted == current_widths: + return + + previous_stretch = header.stretchLastSection() + self._applying_column_layout = True + try: + header.setStretchLastSection(False) + for logical_col, width in zip(logical_cols, fitted, strict=True): + self.table.setColumnWidth(logical_col, width) + finally: + header.setStretchLastSection(previous_stretch) + self._applying_column_layout = False + + def _schedule_default_column_fit(self) -> None: + """Debounce default column fitting after viewport/layout changes.""" + if not self._default_column_fit_timer.isActive(): + self._default_column_fit_timer.start(0) + def _finish_population(self) -> None: """Complete table population - enable sorting, apply column widths, load art.""" try: @@ -1893,17 +2032,26 @@ def _finish_population(self) -> None: # Re-apply header interaction properties (defensive — survives # column-count changes and setSortingEnabled toggling) header.setSectionsMovable(True) - - # Apply saved column widths, or auto-size columns that have none + header.setStretchLastSection(False) + + # Apply saved column widths, or auto-size default columns to + # the current viewport. User-owned layouts are allowed to + # overflow horizontally; generated defaults should not. + use_user_layout = self._has_user_column_layout() + logical_cols: list[int] = [] + widths: list[int] = [] for i in range(start_col, total_cols): col_key = self._col_key_for_logical(i) - if col_key and col_key in self._user_col_widths: - self.table.setColumnWidth(i, self._user_col_widths[col_key]) + if use_user_layout and col_key and col_key in self._user_col_widths: + width = self._user_col_widths[col_key] else: - self.table.setColumnWidth( - i, - self._smart_default_column_width(i), - ) + width = self._smart_default_column_width(i) + logical_cols.append(i) + widths.append(width) + + widths = self._fit_default_widths_to_viewport(logical_cols, widths) + for i, width in zip(logical_cols, widths, strict=True): + self.table.setColumnWidth(i, width) # Restore saved visual column order (from user drag-reorder) if self._user_col_order: @@ -1929,8 +2077,7 @@ def _finish_population(self) -> None: if current_vis != logical: header.moveSection(current_vis, logical) - # Stretch the last column - header.setStretchLastSection(True) + header.setStretchLastSection(False) # Re-install event filter (defensive — survives population) header.installEventFilter(self) @@ -2342,7 +2489,7 @@ def _apply_explicit_cell_visuals(self, cell: QTableWidgetItem, raw_value) -> Non if icon is not None: cell.setIcon(icon) cell.setForeground(_named_qcolor(Colors.DANGER)) - cell.setToolTip("Content Advisory: Explicit") + cell.setToolTip(_("Content Advisory: Explicit")) return if flag == 2: @@ -2350,7 +2497,7 @@ def _apply_explicit_cell_visuals(self, cell: QTableWidgetItem, raw_value) -> Non if icon is not None: cell.setIcon(icon) cell.setForeground(_named_qcolor(Colors.SUCCESS)) - cell.setToolTip("Content Advisory: Clean") + cell.setToolTip(_("Content Advisory: Clean")) return cell.setForeground(_named_qcolor(Colors.TEXT_TERTIARY)) @@ -2375,22 +2522,24 @@ def _update_status(self) -> None: noun = "song" else: noun = "track" - noun_pl = noun + "s" if total != 1 else noun - shown_pl = noun + "s" if shown != 1 else noun if total == 0: self._status_label.setText("") elif shown == total or self._current_filter is None: - self._status_label.setText(f"{total:,} {noun_pl}") + one_template, many_template, _filtered_one, _filtered_many = _COUNT_TEMPLATES[noun] + template = one_template if total == 1 else many_template + self._status_label.setText(_(template).format(count=total)) else: + _one_template, _many_template, filtered_one, filtered_many = _COUNT_TEMPLATES[noun] + template = filtered_one if total == 1 else filtered_many self._status_label.setText( - f"{shown:,} of {total:,} {shown_pl}" + _(template).format(shown=shown, total=total) ) @staticmethod def _get_header(key: str) -> str: """Get display name for a column key.""" if key in COLUMN_CONFIG: - return COLUMN_CONFIG[key][0] + return _(COLUMN_CONFIG[key][0]) return key @staticmethod @@ -2493,6 +2642,7 @@ def eventFilter(self, obj, event): # type: ignore[override] if etype == QEvent.Type.Resize: self._schedule_visible_artwork_load() + self._schedule_default_column_fit() # Wheel events: horizontal trackpad swipe, shift+wheel, normal wheel if etype == QEvent.Type.Wheel: @@ -2611,26 +2761,26 @@ def _on_header_context_menu(self, pos) -> None: # ── "Hide " action ── if clicked_key and clicked_key in COLUMN_CONFIG: - display_name = COLUMN_CONFIG[clicked_key][0] - hide_act = menu.addAction(f"Hide \"{display_name}\"") + display_name = self._get_header(clicked_key) + hide_act = menu.addAction(_("Hide \"{column}\"").format(column=display_name)) if hide_act: hide_act.triggered.connect(lambda _=False, k=clicked_key: self._hide_column(k)) menu.addSeparator() # ── Column sizing actions ── if clicked_key: - resize_act = menu.addAction("Resize Column to Fit") + resize_act = menu.addAction(_("Resize Column to Fit")) if resize_act: resize_act.triggered.connect( lambda _=False, col=clicked_logical: self._resize_column_to_current_content(col) ) - resize_all_act = menu.addAction("Resize All Columns to Fit") + resize_all_act = menu.addAction(_("Resize All Columns to Fit")) if resize_all_act: resize_all_act.triggered.connect(self._resize_all_columns_to_current_content) menu.addSeparator() # ── "Add Column" cascade with grouped sub-menus ── - add_menu = menu.addMenu("Add Column") + add_menu = menu.addMenu(_("Add Column")) if add_menu: add_menu.setStyleSheet(menu.styleSheet()) @@ -2704,11 +2854,11 @@ def _on_header_context_menu(self, pos) -> None: if not avail: continue any_available = True - sub = add_menu.addMenu(group_name) + sub = add_menu.addMenu(_(group_name)) if sub: sub.setStyleSheet(menu.styleSheet()) for key in avail: - display_name = COLUMN_CONFIG[key][0] + display_name = self._get_header(key) act = sub.addAction(display_name) if act: act.triggered.connect(lambda _=False, k=key: self._show_column(k)) @@ -2720,23 +2870,23 @@ def _on_header_context_menu(self, pos) -> None: ] if ungrouped: any_available = True - sub = add_menu.addMenu("Other") + sub = add_menu.addMenu(_("Other")) if sub: sub.setStyleSheet(menu.styleSheet()) for key in ungrouped: - display_name = COLUMN_CONFIG[key][0] + display_name = self._get_header(key) act = sub.addAction(display_name) if act: act.triggered.connect(lambda _=False, k=key: self._show_column(k)) if not any_available: - no_act = add_menu.addAction("(all columns shown)") + no_act = add_menu.addAction(_("(all columns shown)")) if no_act: no_act.setEnabled(False) # ── "Reset Columns" ── menu.addSeparator() - reset_act = menu.addAction("Reset Columns") + reset_act = menu.addAction(_("Reset Columns")) if reset_act: reset_act.triggered.connect(self._reset_columns) @@ -2892,7 +3042,7 @@ def _can_edit_selected_tracks(self, selected: list[dict]) -> bool: return all(track.get("db_track_id") or track.get("db_id") for track in selected) def _edit_action_label(self, selected: list[dict]) -> str: - return f"Edit ({len(selected)})" + return _("Edit ({count})").format(count=len(selected)) def _start_file_drag(self) -> None: """Initiate an async Alt+drag export. @@ -3058,7 +3208,7 @@ def _on_track_context_menu(self, pos) -> None: menu.addSeparator() if len(selected) == 1 and chapter_count_from_data(selected[0].get("chapter_data")) >= 2: - split_act = menu.addAction("Split chapters into individual tracks") + split_act = menu.addAction(_("Split chapters into individual tracks")) if split_act: icon = glyph_icon("chaptered-track", 14, Colors.TEXT_PRIMARY) if icon is not None: @@ -3086,11 +3236,11 @@ def _on_track_context_menu(self, pos) -> None: ) ] - add_menu = menu.addMenu("Add to Playlist") + add_menu = menu.addMenu(_("Add to Playlist")) if add_menu: add_menu.setStyleSheet(menu_style) - new_playlist_act = add_menu.addAction("New Playlist") + new_playlist_act = add_menu.addAction(_("New Playlist")) if new_playlist_act: icon = glyph_icon("plus", 14, Colors.TEXT_PRIMARY) if icon is not None: @@ -3100,7 +3250,7 @@ def _on_track_context_menu(self, pos) -> None: if regular: add_menu.addSeparator() for pl in regular: - title = pl.get("Title", "Untitled") + title = pl.get("Title", _("Untitled")) act = add_menu.addAction(title) if act: act.triggered.connect( @@ -3122,7 +3272,8 @@ def _on_track_context_menu(self, pos) -> None: )): menu.addSeparator() n = len(selected) - label = f"Remove {n} Track{'s' if n != 1 else ''} from Playlist" + template = "Remove {count} Track from Playlist" if n == 1 else "Remove {count} Tracks from Playlist" + label = _(template).format(count=n) remove_act = menu.addAction(label) if remove_act: remove_act.triggered.connect(self._remove_selected_from_playlist) @@ -3130,7 +3281,8 @@ def _on_track_context_menu(self, pos) -> None: # ── "Remove from iPod" ── menu.addSeparator() n_sel = len(selected) - remove_ipod_label = f"Remove {n_sel} Track{'s' if n_sel != 1 else ''} from iPod" + template = "Remove {count} Track from iPod" if n_sel == 1 else "Remove {count} Tracks from iPod" + remove_ipod_label = _(template).format(count=n_sel) remove_ipod_act = menu.addAction(remove_ipod_label) if remove_ipod_act: icon = glyph_icon("minus", 14, Colors.TEXT_PRIMARY) @@ -3144,11 +3296,11 @@ def _on_track_context_menu(self, pos) -> None: if self._is_reorderable_playlist(): selected_rows = sorted({idx.row() for idx in self.table.selectedIndexes()}) menu.addSeparator() - up_act = menu.addAction(f"Move Up\t{_CTRL}+\u2191") + up_act = menu.addAction(f"{_('Move Up')}\t{_CTRL}+\u2191") if up_act: up_act.setEnabled(bool(selected_rows) and selected_rows[0] > 0) up_act.triggered.connect(lambda: self._move_selected_rows(-1)) - down_act = menu.addAction(f"Move Down\t{_CTRL}+\u2193") + down_act = menu.addAction(f"{_('Move Down')}\t{_CTRL}+\u2193") if down_act: down_act.setEnabled(bool(selected_rows) and selected_rows[-1] < self.table.rowCount() - 1) down_act.triggered.connect(lambda: self._move_selected_rows(1)) @@ -3170,10 +3322,10 @@ def _on_track_context_menu(self, pos) -> None: # ── Copy ── menu.addSeparator() - copy_text_act = menu.addAction(f"Copy as Text\t{_CTRL}+C") + copy_text_act = menu.addAction(f"{_('Copy as Text')}\t{_CTRL}+C") if copy_text_act: copy_text_act.triggered.connect(self._copy_selection) - copy_files_act = menu.addAction(f"Copy as File(s)\t{_CTRL}+{_ALT}+C") + copy_files_act = menu.addAction(f"{_('Copy as File(s)')}\t{_CTRL}+{_ALT}+C") if copy_files_act: copy_files_act.triggered.connect(self._copy_files_to_clipboard) @@ -3191,7 +3343,7 @@ def _add_convert_to_podcast_action( if not self._can_edit_selected_tracks(selected): return None - act = menu.addAction("Convert to Podcast") + act = menu.addAction(_("Convert to Podcast")) if act is None: return None @@ -3268,7 +3420,7 @@ def _build_flag_menu(self, menu: QMenu, style: str, selected: list[dict], cache) prefix = "– " new_val = 1 # mixed → on - act = menu.addAction(f"{prefix}{label}") + act = menu.addAction(f"{prefix}{_(label)}") if act: act.triggered.connect( lambda _=False, k=key, v=new_val: self._set_track_flag(k, v) @@ -3286,7 +3438,7 @@ def _build_flag_menu(self, menu: QMenu, style: str, selected: list[dict], cache) else: prefix = "– " new_val = 0 # mixed → check - act = menu.addAction(f"{prefix}Checked") + act = menu.addAction(f"{prefix}{_('Checked')}") if act: act.triggered.connect( lambda _=False, v=new_val: self._set_track_flag("checked_flag", v) @@ -3294,7 +3446,7 @@ def _build_flag_menu(self, menu: QMenu, style: str, selected: list[dict], cache) def _build_rating_menu(self, menu: QMenu, style: str, selected: list[dict], cache) -> None: """Add a Rating submenu with 0-5 star options.""" - rating_menu = menu.addMenu("Rating") + rating_menu = menu.addMenu(_("Rating")) if not rating_menu: return rating_menu.setStyleSheet(style) @@ -3304,13 +3456,13 @@ def _build_rating_menu(self, menu: QMenu, style: str, selected: list[dict], cach unanimous = current_ratings.pop() if len(current_ratings) == 1 else None if unanimous is None and len(selected) > 1: - mixed = rating_menu.addAction("(mixed selection)") + mixed = rating_menu.addAction(_("(mixed selection)")) if mixed: mixed.setEnabled(False) rating_menu.addSeparator() stars = [ - (0, "No Rating"), + (0, _("No Rating")), (20, "★"), (40, "★★"), (60, "★★★"), @@ -3333,7 +3485,7 @@ def _build_content_advisory_menu(self, menu: QMenu, style: str, selected: list[d - 1: explicit - 2: clean """ - advisory_menu = menu.addMenu("Content Advisory") + advisory_menu = menu.addMenu(_("Content Advisory")) if not advisory_menu: return advisory_menu.setStyleSheet(style) @@ -3342,15 +3494,15 @@ def _build_content_advisory_menu(self, menu: QMenu, style: str, selected: list[d unanimous = current_flags.pop() if len(current_flags) == 1 else None if unanimous is None and len(selected) > 1: - mixed = advisory_menu.addAction("(mixed selection)") + mixed = advisory_menu.addAction(_("(mixed selection)")) if mixed: mixed.setEnabled(False) advisory_menu.addSeparator() options: list[tuple[int, str]] = [ - (0, "None (Unset)"), - (1, "Explicit"), - (2, "Clean"), + (0, _("None (Unset)")), + (1, _("Explicit")), + (2, _("Clean")), ] for value, label in options: act = advisory_menu.addAction(label) @@ -3366,7 +3518,7 @@ def _build_content_advisory_menu(self, menu: QMenu, style: str, selected: list[d def _build_volume_menu(self, menu: QMenu, style: str, selected: list[dict]) -> None: """Add a Volume Adjustment submenu with a continuous slider.""" - vol_menu = menu.addMenu("Volume Adjustment") + vol_menu = menu.addMenu(_("Volume Adjustment")) if not vol_menu: return vol_menu.setStyleSheet(style) diff --git a/GUI/widgets/browserChrome.py b/GUI/widgets/browserChrome.py index a942f9b..c32f231 100644 --- a/GUI/widgets/browserChrome.py +++ b/GUI/widgets/browserChrome.py @@ -4,6 +4,8 @@ from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QFrame, QHBoxLayout, QLabel, QSplitter, QVBoxLayout, QWidget +from infrastructure.i18n import tr as _ + from ..styles import FONT_FAMILY, Colors, Metrics, btn_css @@ -73,7 +75,7 @@ def __init__( layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) - self.title_label = QLabel(title, self) + self.title_label = QLabel(_(title), self) self.title_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_TITLE, QFont.Weight.Bold)) self.title_label.setStyleSheet(f""" color:{Colors.TEXT_PRIMARY}; diff --git a/GUI/widgets/gridHeaderBar.py b/GUI/widgets/gridHeaderBar.py index 41ae103..b55a73e 100644 --- a/GUI/widgets/gridHeaderBar.py +++ b/GUI/widgets/gridHeaderBar.py @@ -4,6 +4,8 @@ from PyQt6.QtGui import QAction from PyQt6.QtWidgets import QFrame, QHBoxLayout, QLineEdit, QMenu, QPushButton, QSizePolicy +from infrastructure.i18n import tr as _ + from ..styles import Colors, Metrics, btn_css, input_css # Sort definitions per category: (display_label, sort_key, reverse) @@ -63,7 +65,7 @@ def __init__(self, parent=None): layout.setContentsMargins(8, 0, 8, 0) layout.setSpacing(6) - self._sort_btn = QPushButton(f"Sort: {_DEFAULT_LABEL} \u25be") + self._sort_btn = QPushButton(self._sort_button_text(_DEFAULT_LABEL)) self._sort_btn.setStyleSheet( btn_css(padding="4px 10px", radius=Metrics.BORDER_RADIUS_SM) ) @@ -72,7 +74,7 @@ def __init__(self, parent=None): self._sort_btn.clicked.connect(self._show_sort_menu) self._search = QLineEdit() - self._search.setPlaceholderText("Search\u2026") + self._search.setPlaceholderText(_("Search\u2026")) self._search.setFixedWidth(200) self._search.setStyleSheet( input_css(radius=Metrics.BORDER_RADIUS_SM, padding="4px 10px") @@ -96,7 +98,7 @@ def resetState(self) -> None: self._search.clear() self._search.blockSignals(False) self._active_label = _DEFAULT_LABEL - self._sort_btn.setText(f"Sort: {_DEFAULT_LABEL} \u25be") + self._sort_btn.setText(self._sort_button_text(_DEFAULT_LABEL)) # Emit the default sort so grid is reset even if called from other paths self.sort_changed.emit("title", False) @@ -130,7 +132,7 @@ def _show_sort_menu(self) -> None: all_sorts = _SORTS.get(self._category, _SORTS["Albums"]) for label, key, reverse in all_sorts: - action = cast(QAction, menu.addAction(label)) + action = cast(QAction, menu.addAction(_(label))) action.setCheckable(True) action.setChecked(label == self._active_label) action.triggered.connect( @@ -143,5 +145,9 @@ def _show_sort_menu(self) -> None: def _on_sort_selected(self, label: str, key: str, reverse: bool) -> None: self._active_label = label - self._sort_btn.setText(f"Sort: {label} \u25be") + self._sort_btn.setText(self._sort_button_text(label)) self.sort_changed.emit(key, reverse) + + @staticmethod + def _sort_button_text(label: str) -> str: + return _("Sort: {label} \u25be").format(label=_(label)) diff --git a/GUI/widgets/musicBrowser.py b/GUI/widgets/musicBrowser.py index eb26697..384f626 100644 --- a/GUI/widgets/musicBrowser.py +++ b/GUI/widgets/musicBrowser.py @@ -16,6 +16,7 @@ QVBoxLayout, ) +from infrastructure.i18n import tr as _ from iTunesDB_Shared.constants import ( MEDIA_TYPE_AUDIO, MEDIA_TYPE_AUDIOBOOK, @@ -43,6 +44,12 @@ log = logging.getLogger(__name__) +_SELECTION_TITLE_BY_CATEGORY = { + "Albums": "Select an Album", + "Artists": "Select an Artist", + "Genres": "Select a Genre", +} + if TYPE_CHECKING: from app_core.services import ( DeviceSessionService, @@ -122,6 +129,7 @@ def __init__( # Bottom: Track Browser self.trackContainer = QFrame() + self.trackContainer.setMinimumSize(0, 0) self.trackContainerLayout = QVBoxLayout(self.trackContainer) self.trackContainerLayout.setContentsMargins(0, 0, 0, 0) self.trackContainerLayout.setSpacing(0) @@ -326,7 +334,7 @@ def _refreshCurrentCategory(self): self.browserTrack.clearTable() # Clear track list before reloading self.browserTrack.clearFilter() self.browserTrack.loadTracks(media_type_filter=MEDIA_TYPE_AUDIO) - self.trackListTitleBar.setTitle("All Tracks") + self.trackListTitleBar.setTitle("All Tracks", translate=True) self.trackListTitleBar.resetColor() self.trackListTitleBar.setFullscreenMode(True) elif category == "Playlists": @@ -360,7 +368,7 @@ def _refreshCurrentCategory(self): self.browserTrack.clearTable() self.browserTrack.clearFilter() self.browserTrack.loadTracks(media_type_filter=MEDIA_TYPE_AUDIOBOOK) - self.trackListTitleBar.setTitle(category) + self.trackListTitleBar.setTitle(category, translate=True) self.trackListTitleBar.resetColor() self.trackListTitleBar.setFullscreenMode(True) elif category in ("Videos", "Movies", "TV Shows", "Music Videos"): @@ -377,7 +385,7 @@ def _refreshCurrentCategory(self): self.browserTrack.clearTable() self.browserTrack.clearFilter() self.browserTrack.loadTracks(media_type_filter=_MEDIA_TYPE_FILTER[category]) - self.trackListTitleBar.setTitle(category) + self.trackListTitleBar.setTitle(category, translate=True) self.trackListTitleBar.resetColor() self.trackListTitleBar.setFullscreenMode(True) else: @@ -393,7 +401,10 @@ def _refreshCurrentCategory(self): # won't include video tracks in results. self.browserTrack.loadTracks(media_type_filter=MEDIA_TYPE_AUDIO) self.browserTrack.clearFilter() - self.trackListTitleBar.setTitle(f"Select a{'n' if category[0] in 'AE' else ''} {category[:-1]}") + self.trackListTitleBar.setTitle( + _SELECTION_TITLE_BY_CATEGORY.get(category, f"Select {category}"), + translate=True, + ) self.trackListTitleBar.resetColor() self.trackListTitleBar.setFullscreenMode(False) @@ -451,7 +462,7 @@ def _onGridItemContextRequested(self, items: object, global_pos) -> None: edit_action = None if len(album_items) == 1: - edit_action = menu.addAction("Edit") + edit_action = menu.addAction(_("Edit")) if edit_action is not None: edit_icon = glyph_icon("edit", 14, Colors.TEXT_PRIMARY) if edit_icon is not None: @@ -459,7 +470,7 @@ def _onGridItemContextRequested(self, items: object, global_pos) -> None: if int(album_items[0].get("track_count", 0) or 0) < 1: edit_action.setEnabled(False) - conversion_action = menu.addAction("Convert to a single chaptered track") + conversion_action = menu.addAction(_("Convert to a single chaptered track")) if conversion_action is None: return @@ -476,7 +487,7 @@ def _onGridItemContextRequested(self, items: object, global_pos) -> None: unify_context = self._album_artwork_unify_context(album_items[0]) if unify_context is not None: menu.addSeparator() - unify_action = menu.addAction("Unify Artwork") + unify_action = menu.addAction(_("Unify Artwork")) if unify_action is not None: unify_icon = glyph_icon("photo", 14, Colors.TEXT_PRIMARY) if unify_icon is not None: @@ -547,8 +558,8 @@ def _show_unify_artwork_dialog( except Exception as exc: QMessageBox.warning( self, - "Unify Artwork", - f"Could not prepare artwork:\n\n{exc}", + _("Unify Artwork"), + _("Could not prepare artwork:\n\n{error}").format(error=exc), ) return @@ -569,8 +580,8 @@ def _show_unify_artwork_dialog( pass QMessageBox.warning( self, - "Unify Artwork", - f"Could not stage artwork update:\n\n{exc}", + _("Unify Artwork"), + _("Could not stage artwork update:\n\n{error}").format(error=exc), ) def refresh_artwork_appearance(self) -> None: diff --git a/GUI/widgets/photoBrowser.py b/GUI/widgets/photoBrowser.py index b4d0dd9..d8a691e 100644 --- a/GUI/widgets/photoBrowser.py +++ b/GUI/widgets/photoBrowser.py @@ -22,6 +22,7 @@ QWidget, ) +from infrastructure.i18n import tr as _ from ipod_device.artwork import ITHMB_FORMAT_MAP from SyncEngine.photos import ( PhotoDB, @@ -78,6 +79,11 @@ _PNG_EXTENSIONS = {".png"} +def _album_display_label(name: str) -> str: + """Translate app-owned album labels without touching user album names.""" + return _("All Photos") if name == "All Photos" else name + + def _safe_photo_stem(name: str, fallback: str) -> str: stem = Path(name).stem if name else fallback cleaned = re.sub(r"[^A-Za-z0-9._ -]+", "_", stem).strip(" ._") @@ -419,7 +425,7 @@ def _build_ui(self): header = BrowserHeroHeader("Photos", self) - self.new_album_btn = QPushButton("New Album") + self.new_album_btn = QPushButton(_("New Album")) self.new_album_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.new_album_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.new_album_btn.setStyleSheet(chrome_action_btn_css()) @@ -470,15 +476,15 @@ def _build_ui(self): self.viewer = PhotoViewerPane( heading="", - empty_title="No photo selected", - empty_summary="Select a photo to inspect its preview and album details.", + empty_title=_("No photo selected"), + empty_summary=_("Select a photo to inspect its preview and album details."), parent=splitter, ) viewer_actions = self.viewer.configureActionRow([ - ("export_photo", "Export", "download", False), - ("add_to_album", "Add to Album", "plus", False), - ("remove_from_album", "Remove from Album", "minus", False), - ("delete_photo", "Delete Photo", "trash", True), + ("export_photo", _("Export"), "download", False), + ("add_to_album", _("Add to Album"), "plus", False), + ("remove_from_album", _("Remove from Album"), "minus", False), + ("delete_photo", _("Delete Photo"), "trash", True), ]) self.export_photo_btn = viewer_actions["export_photo"] self.add_to_album_btn = viewer_actions["add_to_album"] @@ -810,7 +816,7 @@ def _format_meta_sections( sections: list[tuple[str, list[tuple[str, str]]]] = [] album_names = sorted(name for name in getattr(photo, "album_names", set()) if name) - album_label = ", ".join(album_names) if album_names else "All Photos" + album_label = ", ".join(album_names) if album_names else _("All Photos") image_rows = [ ("Image ID", str(photo.image_id)), ("Display Name", photo.display_name or self._device_photo_title(photo)), @@ -1094,7 +1100,7 @@ def _process_thumb_batch(self) -> None: batch: list[tuple[int, PhotoEntry, int | None]] = [] load_token = self._grid_load_token - for _ in range(_THUMB_DECODE_BATCH_SIZE): + for _batch_index in range(_THUMB_DECODE_BATCH_SIZE): if not self._thumb_queue: break photo_id, photo, format_id, token = self._thumb_queue.popleft() @@ -1176,7 +1182,7 @@ def _rebuild_album_sidebar(self, album_names: list[str]): self._album_inner_layout.addStretch() def _add_album_button(self, name: str): - btn = QPushButton(name, self._album_inner) + btn = QPushButton(_album_display_label(name), self._album_inner) btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) btn.setStyleSheet(sidebar_nav_css()) @@ -1234,7 +1240,7 @@ def _on_photo_context_requested( export_action = self._add_menu_action( menu, - "Export Photo...", + _("Export Photo..."), glyph_name="download", enabled=not actions_locked, ) @@ -1242,7 +1248,7 @@ def _on_photo_context_requested( menu.addSeparator() add_action = self._add_menu_action( menu, - "Add to Album", + _("Add to Album"), glyph_name="plus", enabled=not actions_locked and bool(self._available_album_targets(photo)), ) @@ -1252,7 +1258,7 @@ def _on_photo_context_requested( if current_album: remove_action = self._add_menu_action( menu, - "Remove from Current Album", + _("Remove from Current Album"), glyph_name="minus", enabled=( not actions_locked @@ -1263,7 +1269,7 @@ def _on_photo_context_requested( menu.addSeparator() delete_action = self._add_menu_action( menu, - "Delete Photo", + _("Delete Photo"), glyph_name="trash", color=Colors.DANGER, enabled=not actions_locked, @@ -1292,7 +1298,7 @@ def _on_album_context_requested(self, album_name: str, global_pos) -> None: export_action = self._add_menu_action( menu, - "Export Album..." if target_album else "Export All Photos...", + _("Export Album...") if target_album else _("Export All Photos..."), glyph_name="download", enabled=not actions_locked and exportable_count > 0, ) @@ -1301,7 +1307,7 @@ def _on_album_context_requested(self, album_name: str, global_pos) -> None: new_action = self._add_menu_action( menu, - "New Album", + _("New Album"), glyph_name="plus", enabled=not actions_locked, ) @@ -1312,14 +1318,14 @@ def _on_album_context_requested(self, album_name: str, global_pos) -> None: menu.addSeparator() rename_action = self._add_menu_action( menu, - "Rename Album", + _("Rename Album"), glyph_name="edit", enabled=not actions_locked, ) delete_action = self._add_menu_action( menu, - "Delete Album", + _("Delete Album"), glyph_name="trash", color=Colors.DANGER, enabled=not actions_locked, @@ -1448,7 +1454,11 @@ def _start_photo_write( ipod_path = self._current_device_path() if not ipod_path: - QMessageBox.warning(self, "No iPod Connected", "Select an iPod before editing device photos.") + QMessageBox.warning( + self, + _("No iPod Connected"), + _("Select an iPod before editing device photos."), + ) return self._show_save_indicator("saving") @@ -1566,12 +1576,16 @@ def _start_photo_export( QMessageBox.information(self, "Sync Running", "Wait for the current sync to finish before exporting photos.") return if not exports: - QMessageBox.information(self, "No Photos", "There are no photos to export.") + QMessageBox.information(self, _("No Photos"), _("There are no photos to export.")) return ipod_path = self._current_device_path() if not ipod_path: - QMessageBox.warning(self, "No iPod Connected", "Select an iPod before exporting device photos.") + QMessageBox.warning( + self, + _("No iPod Connected"), + _("Select an iPod before exporting device photos."), + ) return worker = _PhotoExportWorker( @@ -1611,14 +1625,18 @@ def _export_current_photo(self): if photo is None: return if not self._current_device_path(): - QMessageBox.warning(self, "No iPod Connected", "Select an iPod before exporting device photos.") + QMessageBox.warning( + self, + _("No iPod Connected"), + _("Select an iPod before exporting device photos."), + ) return title = self._device_photo_title(photo) default_path = Path.home() / _default_export_filename(title, int(photo.image_id)) path, selected_filter = QFileDialog.getSaveFileName( self, - "Export Photo", + _("Export Photo"), str(default_path), _EXPORT_FILTERS, ) @@ -1634,13 +1652,17 @@ def _export_current_photo(self): def _export_album_target(self, album_name: str) -> None: photos = self._photos_for_album_target(album_name) if not photos: - QMessageBox.information(self, "No Photos", "There are no photos to export.") + QMessageBox.information(self, _("No Photos"), _("There are no photos to export.")) return if not self._current_device_path(): - QMessageBox.warning(self, "No iPod Connected", "Select an iPod before exporting device photos.") + QMessageBox.warning( + self, + _("No iPod Connected"), + _("Select an iPod before exporting device photos."), + ) return - title = "Export Album" if album_name else "Export All Photos" + title = _("Export Album") if album_name else _("Export All Photos") folder = QFileDialog.getExistingDirectory( self, title, @@ -1656,7 +1678,7 @@ def _export_album_target(self, album_name: str) -> None: ) def _create_album(self): - name, ok = QInputDialog.getText(self, "New Album", "Album name:") + name, ok = QInputDialog.getText(self, _("New Album"), _("Album name:")) if ok and name.strip(): self._start_photo_write("create_album", album_name=name.strip()) @@ -1666,12 +1688,16 @@ def _add_to_album(self): return album_names = self._available_album_targets(photo) if not album_names: - QMessageBox.information(self, "No Available Albums", "Create another album first, or choose a photo that is not already in every album.") + QMessageBox.information( + self, + _("No Available Albums"), + _("Create another album first, or choose a photo that is not already in every album."), + ) return target_album, ok = QInputDialog.getItem( self, - "Add Photo to Album", - "Album:", + _("Add Photo to Album"), + _("Album:"), album_names, 0, False, @@ -1686,7 +1712,7 @@ def _rename_album(self): def _rename_album_target(self, current: str) -> None: if not current: return - new_name, ok = QInputDialog.getText(self, "Rename Album", "New album name:", text=current) + new_name, ok = QInputDialog.getText(self, _("Rename Album"), _("New album name:"), text=current) if ok and new_name.strip() and new_name.strip() != current: self._start_photo_write("rename_album", old_name=current, new_name=new_name.strip()) @@ -1697,7 +1723,11 @@ def _delete_album(self): def _delete_album_target(self, current: str) -> None: if not current: return - if QMessageBox.question(self, "Delete Album", f"Delete '{current}' from the iPod now?") == QMessageBox.StandardButton.Yes: + if QMessageBox.question( + self, + _("Delete Album"), + _("Delete '{name}' from the iPod now?").format(name=current), + ) == QMessageBox.StandardButton.Yes: self._start_photo_write("delete_album", album_name=current) def _remove_from_album(self): @@ -1713,7 +1743,9 @@ def _delete_photo(self): return if QMessageBox.question( self, - "Delete Photo", - f"Delete '{self._device_photo_title(photo)}' from the iPod now?", + _("Delete Photo"), + _("Delete '{name}' from the iPod now?").format( + name=self._device_photo_title(photo) + ), ) == QMessageBox.StandardButton.Yes: self._start_photo_write("delete_photo", image_id=photo.image_id) diff --git a/GUI/widgets/playlistBrowser.py b/GUI/widgets/playlistBrowser.py index 5a0da6d..b125877 100644 --- a/GUI/widgets/playlistBrowser.py +++ b/GUI/widgets/playlistBrowser.py @@ -37,6 +37,7 @@ PlaylistWriteWorker as _PlaylistWriteWorker, ) from app_core.runtime import display_playlists_from_rows +from infrastructure.i18n import tr as _ from iTunesDB_Shared.constants import MHOD_TYPE_TITLE from iTunesDB_Shared.playlist_properties import playlist_description_from_row @@ -175,6 +176,56 @@ def _mhsd_type_label(playlist: dict | None) -> str: return " + ".join(f"MHSD type {dataset_type}" for dataset_type in types) +_IPOD_CATEGORY_TITLES_BY_MHSD5 = { + 2: "Movies", + 3: "TV Shows", + 4: "Music", + 5: "Audiobooks", + 6: "Ringtones", + 7: "Rentals", +} + +_IPOD_CATEGORY_TITLES_BY_RAW_TITLE = { + "audiobooks": "Audiobooks", + "movies": "Movies", + "music": "Music", + "podcasts": "Podcasts", + "rentals": "Rentals", + "ringtones": "Ringtones", + "tv shows": "TV Shows", + "videos": "Videos", + "有声书": "Audiobooks", + "电影": "Movies", + "电视节目": "TV Shows", + "铃声": "Ringtones", + "视频": "Videos", + "音乐": "Music", +} + + +def _canonical_ipod_category_title(playlist: dict, raw_title: str) -> str: + """Return a stable English msgid for an iPod built-in category row.""" + mhsd5_type = _mhsd5_type_value(playlist) + if mhsd5_type in _IPOD_CATEGORY_TITLES_BY_MHSD5: + return _IPOD_CATEGORY_TITLES_BY_MHSD5[mhsd5_type] + normalized_title = raw_title.strip().casefold() + return _IPOD_CATEGORY_TITLES_BY_RAW_TITLE.get(normalized_title, raw_title) + + +def _playlist_display_title(playlist: dict | None) -> tuple[str, bool]: + """Return the playlist title and whether it is an app-owned label.""" + raw_title = "" + if playlist: + raw_title = str(playlist.get("Title") or "").strip() + if playlist and playlist.get("master_flag") and not _is_ipod_category_playlist(playlist): + return "Library (Master)", True + if playlist and _is_ipod_category_playlist(playlist): + return _canonical_ipod_category_title(playlist, raw_title) or "Untitled", True + if raw_title: + return raw_title, False + return "Untitled", True + + def _int_value(value: object) -> int: if value is None: return 0 @@ -380,7 +431,7 @@ def __init__(self): title_col.setContentsMargins(0, 0, 0, 0) title_col.setSpacing(4) - self.title_label = QLabel("Select a playlist") + self.title_label = QLabel(_("Select a playlist")) self.title_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_PAGE_TITLE, QFont.Weight.Bold)) self.title_label.setStyleSheet(_label_css(Colors.TEXT_PRIMARY)) self.title_label.setWordWrap(True) @@ -415,7 +466,7 @@ def __init__(self): btn_row.setContentsMargins(0, 0, 0, 0) btn_row.setSpacing(6) - self.edit_btn = QPushButton("Edit") + self.edit_btn = QPushButton(_("Edit")) self.edit_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.edit_btn.setCursor(Qt.CursorShape.PointingHandCursor) _ed_ic = glyph_icon("edit", (14), Colors.TEXT_SECONDARY) @@ -433,7 +484,7 @@ def __init__(self): self.edit_btn.hide() btn_row.addWidget(self.edit_btn) - self.delete_btn = QPushButton("Delete") + self.delete_btn = QPushButton(_("Delete")) self.delete_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.delete_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.delete_btn.setStyleSheet(btn_css( @@ -447,7 +498,7 @@ def __init__(self): self.delete_btn.hide() btn_row.addWidget(self.delete_btn) - self.evaluate_btn = QPushButton("Evaluate Now") + self.evaluate_btn = QPushButton(_("Evaluate Now")) self.evaluate_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.evaluate_btn.setCursor(Qt.CursorShape.PointingHandCursor) _eval_ic = glyph_icon("check-circle", (14), Colors.TEXT_SECONDARY) @@ -469,7 +520,7 @@ def __init__(self): self.evaluate_btn.hide() btn_row.addWidget(self.evaluate_btn) - self.export_btn = QPushButton("Export") + self.export_btn = QPushButton(_("Export")) self.export_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.export_btn.setCursor(Qt.CursorShape.PointingHandCursor) _exp_ic = glyph_icon("arrow-up-tray", (14), Colors.TEXT_SECONDARY) @@ -524,7 +575,7 @@ def __init__(self): rules_header = QHBoxLayout() rules_header.setContentsMargins(0, 0, 0, 0) rules_header.setSpacing(8) - rules_title = QLabel("Rules") + rules_title = QLabel(_("Rules")) rules_title.setFont(QFont(FONT_FAMILY, Metrics.FONT_XS, QFont.Weight.Bold)) rules_title.setStyleSheet(_subtle_label_css(Colors.TEXT_SECONDARY)) rules_header.addWidget(rules_title) @@ -557,7 +608,7 @@ def __init__(self): details_header = QHBoxLayout(details_header_widget) details_header.setContentsMargins(0, 0, 0, 0) details_header.setSpacing(8) - details_label = QLabel("Details", details_header_widget) + details_label = QLabel(_("Details"), details_header_widget) details_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_XS, QFont.Weight.Bold)) details_label.setStyleSheet(_subtle_label_css(Colors.TEXT_SECONDARY)) details_header.addWidget(details_label) @@ -582,7 +633,7 @@ def _add_metric(self, label: str) -> QLabel: group.setContentsMargins(0, 0, 0, 0) group.setSpacing(2) - label_widget = QLabel(label) + label_widget = QLabel(_(label)) label_widget.setFont(QFont(FONT_FAMILY, Metrics.FONT_XS, QFont.Weight.Bold)) label_widget.setStyleSheet(_subtle_label_css(Colors.TEXT_SECONDARY)) group.addWidget(label_widget) @@ -1003,12 +1054,12 @@ def showEmpty(self) -> None: self._clear_details() self._clear_rules_preview() self._rules_panel.hide() - self.title_label.setText("Select a playlist") + self.title_label.setText(_("Select a playlist")) self.description_label.setText("") self.description_label.hide() self.type_label.setText("") self.type_label.hide() - self._source_label.setText("Choose a playlist to inspect its tracks and database metadata") + self._source_label.setText(_("Choose a playlist to inspect its tracks and database metadata")) self.stats_label.setText("") for metric in (self._track_metric, self._duration_metric, self._size_metric, self._sort_metric): metric.setText("—") @@ -1045,7 +1096,7 @@ def _add_section_header(self, text: str) -> None: self.details_layout.addWidget(sep) self._detail_labels.append(sep) - lbl = QLabel(text.upper()) + lbl = QLabel(_(text).upper()) lbl.setFont(QFont(FONT_FAMILY, Metrics.FONT_XS, QFont.Weight.Bold)) lbl.setStyleSheet( f"color: {Colors.TEXT_SECONDARY}; background: transparent;" @@ -1057,7 +1108,7 @@ def _add_section_header(self, text: str) -> None: def _add_detail_text(self, text: str) -> None: """Add a plain text line to details (used for rule summaries).""" - lbl = QLabel(text) + lbl = QLabel(_(text)) lbl.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) lbl.setStyleSheet(f"color: {Colors.TEXT_SECONDARY}; background: transparent; border: none;") lbl.setWordWrap(True) @@ -1173,7 +1224,7 @@ def loadPlaylists(self, playlists: list[dict]) -> None: empty_icon.setStyleSheet("background: transparent; border: none;") empty_vbox.addWidget(empty_icon) - empty_text = QLabel("No playlists on this iPod") + empty_text = QLabel(_("No playlists on this iPod")) empty_text.setFont(QFont(FONT_FAMILY, Metrics.FONT_MD)) empty_text.setStyleSheet(f"color: {Colors.TEXT_TERTIARY}; background: transparent; border: none;") empty_text.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -1224,7 +1275,7 @@ def _add_section(self, text: str) -> None: spacer.setStyleSheet("background: transparent; border: none;") self._inner_layout.addWidget(spacer) return - lbl = QLabel(text) + lbl = QLabel(_(text)) lbl.setFont(QFont(FONT_FAMILY, Metrics.FONT_XS, QFont.Weight.Bold)) lbl.setStyleSheet( f"color: {Colors.TEXT_TERTIARY}; background: transparent; " @@ -1233,21 +1284,17 @@ def _add_section(self, text: str) -> None: self._inner_layout.addWidget(lbl) def _add_playlist_button(self, playlist: dict, icon_name: str, dimmed: bool = False) -> None: - title = playlist.get("Title", "Untitled") count = playlist.get("mhip_child_count", 0) - is_master = bool(playlist.get("master_flag")) + display_title, translate_title = _playlist_display_title(playlist) + display_label = _(display_title) if translate_title else display_title - display_title = title - if is_master: - display_title = "Library (Master)" - - btn_text = display_title + btn_text = display_label if count > 0: btn_text += f" ({count})" btn = QPushButton(btn_text) btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) - btn.setToolTip(f"{title}\n{count} tracks\n{_mhsd_type_label(playlist)}") + btn.setToolTip(f"{display_label}\n{count} {_('tracks')}\n{_mhsd_type_label(playlist)}") fg = Colors.TEXT_DISABLED if dimmed else Colors.TEXT_PRIMARY ic = glyph_icon(icon_name, (20), fg) @@ -1327,14 +1374,14 @@ def __init__( self._header = BrowserHeroHeader("Playlists", self) root.addWidget(self._header) - self._new_playlist_btn = QPushButton("New Playlist") + self._new_playlist_btn = QPushButton(_("New Playlist")) self._new_playlist_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM, QFont.Weight.Bold)) self._new_playlist_btn.setCursor(Qt.CursorShape.PointingHandCursor) self._new_playlist_btn.setStyleSheet(chrome_action_btn_css()) self._new_playlist_btn.clicked.connect(self._onNewPlaylistButton) self._header.actions_layout.addWidget(self._new_playlist_btn) - self._import_playlist_btn = QPushButton("Import Playlist") + self._import_playlist_btn = QPushButton(_("Import Playlist")) self._import_playlist_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self._import_playlist_btn.setCursor(Qt.CursorShape.PointingHandCursor) self._import_playlist_btn.setStyleSheet(chrome_action_btn_css()) @@ -1397,7 +1444,7 @@ def __init__( _imp_lay.setSpacing(12) _imp_lay.addStretch() - _imp_title = QLabel("Importing Playlist\u2026") + _imp_title = QLabel(_("Importing Playlist\u2026")) _imp_title.setFont(QFont(FONT_FAMILY, Metrics.FONT_PAGE_TITLE, QFont.Weight.Bold)) _imp_title.setStyleSheet(f"color: {Colors.TEXT_PRIMARY}; background: transparent;") _imp_title.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -1509,7 +1556,7 @@ def loadPlaylists(self) -> None: self.infoCard.showEmpty() self.trackList.clearTable() self._set_empty_regular_playlist_notice(None, 0) - self.trackTitleBar.setTitle("Select a playlist") + self.trackTitleBar.setTitle("Select a playlist", translate=True) self.trackTitleBar.resetColor() self._current_playlist = None @@ -1543,7 +1590,7 @@ def clear(self) -> None: self.infoCard.showEmpty() self.trackList.clearTable(clear_cache=True) self._set_empty_regular_playlist_notice(None, 0) - self.trackTitleBar.setTitle("Select a playlist") + self.trackTitleBar.setTitle("Select a playlist", translate=True) self.trackTitleBar.resetColor() self._current_playlist = None self._playlist_signature = None @@ -1599,7 +1646,7 @@ def _switchToBrowse(self) -> None: def _set_import_busy(self, busy: bool) -> None: self._import_playlist_btn.setEnabled(not busy) self._new_playlist_btn.setEnabled(not busy) - self._import_playlist_btn.setText("Importing…" if busy else "Import Playlist") + self._import_playlist_btn.setText(_("Importing…") if busy else _("Import Playlist")) def _onNewPlaylistButton(self) -> None: dlg = NewPlaylistDialog(self) @@ -1631,10 +1678,8 @@ def _onPlaylistSelected(self, playlist: dict) -> None: self.infoCard.showPlaylist(playlist, resolved_tracks, track_id_index) # Update title bar - title = playlist.get("Title", "Untitled") - if playlist.get("master_flag") and not _is_ipod_category_playlist(playlist): - title = "Library (Master)" - self.trackTitleBar.setTitle(title) + title, translate_title = _playlist_display_title(playlist) + self.trackTitleBar.setTitle(title, translate=translate_title) # Color the title bar based on playlist type if _is_ipod_category_playlist(playlist): @@ -1663,14 +1708,14 @@ def _onNewPlaylist(self, kind: str) -> None: ) self.editor.new_playlist() self._switchToEditor(1) - self.trackTitleBar.setTitle("New Smart Playlist") + self.trackTitleBar.setTitle("New Smart Playlist", translate=True) self.trackTitleBar.setColor(*Colors.PLAYLIST_SMART) self.trackList.clearTable() self._set_empty_regular_playlist_notice(None, 0) else: self.regularEditor.new_playlist() self._switchToEditor(2) - self.trackTitleBar.setTitle("New Playlist") + self.trackTitleBar.setTitle("New Playlist", translate=True) self.trackTitleBar.resetColor() self.trackList.clearTable() self._set_empty_regular_playlist_notice(None, 0) @@ -1734,8 +1779,8 @@ def _onEditorSaved(self, playlist_data: dict) -> None: # Select the saved playlist in the list (if it has an ID) self.infoCard.showPlaylist(playlist_data, []) - title = playlist_data.get("Title", "Untitled") - self.trackTitleBar.setTitle(title) + title, translate_title = _playlist_display_title(playlist_data) + self.trackTitleBar.setTitle(title, translate=translate_title) if _is_user_smart_playlist(playlist_data): self.trackTitleBar.setColor(*Colors.PLAYLIST_SMART) self._set_empty_regular_playlist_notice(None, 0) @@ -1807,7 +1852,7 @@ def _onDeleteDone(self, playlist_name: str) -> None: self.infoCard.showEmpty() self.trackList.clearTable() self._set_empty_regular_playlist_notice(None, 0) - self.trackTitleBar.setTitle("Select a playlist") + self.trackTitleBar.setTitle("Select a playlist", translate=True) self.trackTitleBar.resetColor() def _onDeleteFailed(self, error_msg: str) -> None: @@ -1834,7 +1879,7 @@ def _writePlaylistToIPod(self, playlist: dict) -> None: # Show a saving indicator on the info card self.infoCard.edit_btn.setEnabled(False) self.infoCard.evaluate_btn.setEnabled(False) - self.infoCard.evaluate_btn.setText("Writing…") + self.infoCard.evaluate_btn.setText(_("Writing…")) self.infoCard.evaluate_btn.setVisible(True) self._eval_worker = _PlaylistWriteWorker( @@ -1850,7 +1895,7 @@ def _onWriteDone(self, matched_count: int, playlist_name: str) -> None: """Playlist write completed successfully.""" self.infoCard.edit_btn.setEnabled(True) self.infoCard.evaluate_btn.setEnabled(True) - self.infoCard.evaluate_btn.setText("Evaluate Now") + self.infoCard.evaluate_btn.setText(_("Evaluate Now")) # Re-evaluate visibility (evaluate is only for smart playlists) if not _is_user_smart_playlist(self._current_playlist): self.infoCard.evaluate_btn.setVisible(False) @@ -1875,7 +1920,7 @@ def _onWriteFailed(self, error_msg: str) -> None: """Playlist write failed.""" self.infoCard.edit_btn.setEnabled(True) self.infoCard.evaluate_btn.setEnabled(True) - self.infoCard.evaluate_btn.setText("Evaluate Now") + self.infoCard.evaluate_btn.setText(_("Evaluate Now")) if not _is_user_smart_playlist(self._current_playlist): self.infoCard.evaluate_btn.setVisible(False) @@ -1968,9 +2013,9 @@ def _onImportPlaylist(self) -> None: QMessageBox.warning(self, "No Device", "Please connect an iPod first.") return - path, _ = QFileDialog.getOpenFileName( + path, _selected_filter = QFileDialog.getOpenFileName( self, - "Import Playlist", + _("Import Playlist"), "", "Playlist Files (*.m3u *.m3u8 *.pls *.xspf);;All Files (*)", ) @@ -1982,7 +2027,7 @@ def _onImportPlaylist(self) -> None: self._set_import_busy(True) self._topStack.setCurrentIndex(3) self._import_progress_bar.setRange(0, 0) # indeterminate - self._import_status_label.setText("Parsing playlist…") + self._import_status_label.setText(_("Parsing playlist…")) self._import_count_label.setText("") self._import_worker = _PlaylistImportWorker( diff --git a/GUI/widgets/podcastBrowser.py b/GUI/widgets/podcastBrowser.py index 29b3066..6726509 100644 --- a/GUI/widgets/podcastBrowser.py +++ b/GUI/widgets/podcastBrowser.py @@ -72,6 +72,8 @@ QWidget, ) +from infrastructure.i18n import tr + from ..glyphs import glyph_icon, glyph_pixmap from ..hidpi import scale_pixmap_for_display from ..styles import ( @@ -228,11 +230,11 @@ def _load_artwork_bytes(source: str) -> bytes | None: def _status_accent(status: str) -> str: - if status == "On iPod": + if status in {"On iPod", tr("On iPod")}: return Colors.SUCCESS - if status == "Downloaded": + if status in {"Downloaded", tr("Downloaded")}: return Colors.ACCENT - if "Downloading" in status: + if "Downloading" in status or tr("Downloading") in status: return Colors.WARNING return Colors.TEXT_TERTIARY @@ -255,7 +257,30 @@ def _episode_meta_text(row: dict) -> str: status = str(row.get("ep_status") or "") if status and not _is_state_status(status) and status not in parts: parts.append(status) - return " | ".join(parts) if parts else "Episode" + return " | ".join(parts) if parts else tr("Episode") + + +def _set_combo_options(combo: QComboBox, options: list[str]) -> None: + combo.clear() + for option in options: + combo.addItem(tr(option), option) + + +def _combo_value(combo: QComboBox) -> str: + value = combo.itemData(combo.currentIndex(), Qt.ItemDataRole.UserRole) + if isinstance(value, str): + return value + return combo.currentText() + + +def _set_combo_value(combo: QComboBox, value: str) -> None: + for index in range(combo.count()): + if combo.itemData(index, Qt.ItemDataRole.UserRole) == value: + combo.setCurrentIndex(index) + return + idx = combo.findText(value) + if idx >= 0: + combo.setCurrentIndex(idx) def _wrap_lines( @@ -421,9 +446,9 @@ def __init__(self, parent: QWidget | None = None) -> None: self._action_row.setObjectName("podcastEpisodeActionRow") self._action_row.setFixedHeight(_EPISODE_ACTION_ROW_HEIGHT) - self._add_btn = _PodcastCardMouseButton("Add to iPod", self._action_row) + self._add_btn = _PodcastCardMouseButton(tr("Add to iPod"), self._action_row) self._add_btn.setObjectName("podcastEpisodeAddButton") - self._add_btn.setToolTip("Add this episode to iPod") + self._add_btn.setToolTip(tr("Add this episode to iPod")) self._add_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM, QFont.Weight.DemiBold)) self._add_btn.setStyleSheet( btn_css( @@ -441,7 +466,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self._add_btn.setIcon(add_icon) self._add_btn.setIconSize(QSize(13, 13)) add_metrics = QFontMetrics(self._add_btn.font()) - self._add_btn_full_text = "Add to iPod" + self._add_btn_full_text = tr("Add to iPod") self._add_btn_full_width = add_metrics.horizontalAdvance( self._add_btn_full_text ) + 34 @@ -452,11 +477,11 @@ def __init__(self, parent: QWidget | None = None) -> None: self._add_btn.clicked.connect(lambda: self.add_requested.emit(self._row_index)) self._remove_btn = _PodcastCardMouseButton( - "Remove from iPod", + tr("Remove from iPod"), self._action_row, ) self._remove_btn.setObjectName("podcastEpisodeRemoveButton") - self._remove_btn.setToolTip("Remove this episode from iPod") + self._remove_btn.setToolTip(tr("Remove this episode from iPod")) self._remove_btn.setFont( QFont(FONT_FAMILY, Metrics.FONT_SM, QFont.Weight.DemiBold) ) @@ -476,7 +501,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self._remove_btn.setIcon(remove_icon) self._remove_btn.setIconSize(QSize(13, 13)) remove_metrics = QFontMetrics(self._remove_btn.font()) - self._remove_btn_full_text = "Remove from iPod" + self._remove_btn_full_text = tr("Remove from iPod") self._remove_btn_full_width = remove_metrics.horizontalAdvance( self._remove_btn_full_text ) + 34 @@ -488,7 +513,7 @@ def __init__(self, parent: QWidget | None = None) -> None: lambda: self.remove_requested.emit(self._row_index) ) - self._more_btn = _PodcastCardMouseButton("More", self._action_row) + self._more_btn = _PodcastCardMouseButton(tr("More"), self._action_row) self._more_btn.setObjectName("podcastEpisodeMoreButton") self._more_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM, QFont.Weight.DemiBold)) self._more_btn.setStyleSheet( @@ -497,8 +522,8 @@ def __init__(self, parent: QWidget | None = None) -> None: btn_metrics = QFontMetrics(self._more_btn.font()) self._more_btn.setFixedSize( max( - btn_metrics.horizontalAdvance("More"), - btn_metrics.horizontalAdvance("Show less"), + btn_metrics.horizontalAdvance(tr("More")), + btn_metrics.horizontalAdvance(tr("Show less")), ) + 24, _EPISODE_ACTION_ROW_HEIGHT, @@ -556,19 +581,19 @@ def bind( podcast_title = str(row.get("podcast_feed_title") or "") self._podcast_label.setText(podcast_title) self._podcast_label.setVisible(bool(podcast_title)) - self._title_label.setText(str(row.get("Title") or "Untitled Episode")) + self._title_label.setText(str(row.get("Title") or tr("Untitled Episode"))) self._meta_label.setText(_episode_meta_text(row)) - self._description_label.setText(description_text or "No description available.") + self._description_label.setText(description_text or tr("No description available.")) self._set_description_height(description_text, expanded) status = str(row.get("ep_status") or "") if _is_state_status(status): - self._status_label.setText(status) + self._status_label.setText(tr(status)) self._status_label.show() else: self._status_label.hide() - self._more_btn.setText("Show less" if expanded else "More") + self._more_btn.setText(tr("Show less") if expanded else tr("More")) self._more_btn.setVisible(show_more) self._add_btn.setVisible(bool(row.get("_can_add_to_ipod"))) self._remove_btn.setVisible(bool(row.get("_can_remove_from_ipod"))) @@ -609,7 +634,7 @@ def _title_height_for_width(self, width: int) -> int: metrics = QFontMetrics(self._title_label.font()) if width <= 0: return metrics.lineSpacing() - text = self._title_label.text() or "Untitled Episode" + text = self._title_label.text() or tr("Untitled Episode") bounds = metrics.boundingRect( QRect(0, 0, width, 200), Qt.TextFlag.TextWordWrap, @@ -1413,7 +1438,7 @@ def _build_toolbar(self) -> QWidget: bar = BrowserHeroHeader("Podcasts", self) layout = bar.actions_layout - self._add_btn = QPushButton("Add Podcast") + self._add_btn = QPushButton(tr("Add Podcast")) self._add_btn.setFont(QFont(FONT_FAMILY, (Metrics.FONT_SM))) self._add_btn.setStyleSheet(chrome_action_btn_css()) _add_ic = glyph_icon("plus", (14), Colors.TEXT_PRIMARY) @@ -1423,7 +1448,7 @@ def _build_toolbar(self) -> QWidget: self._add_btn.clicked.connect(self._on_search) layout.addWidget(self._add_btn) - self._refresh_btn = QPushButton("Refresh All") + self._refresh_btn = QPushButton(tr("Refresh All")) self._refresh_btn.setFont(QFont(FONT_FAMILY, (Metrics.FONT_SM))) self._refresh_btn.setStyleSheet(chrome_action_btn_css()) _refresh_ic = glyph_icon("refresh", (14), Colors.TEXT_PRIMARY) @@ -1433,7 +1458,7 @@ def _build_toolbar(self) -> QWidget: self._refresh_btn.clicked.connect(self._on_refresh_all) layout.addWidget(self._refresh_btn) - self._sync_btn = QPushButton("Sync Podcasts") + self._sync_btn = QPushButton(tr("Sync Podcasts")) self._sync_btn.setFont(QFont(FONT_FAMILY, (Metrics.FONT_SM))) self._sync_btn.setStyleSheet(chrome_action_btn_css()) _sync_ic = glyph_icon("refresh", (14), Colors.TEXT_PRIMARY) @@ -1441,8 +1466,7 @@ def _build_toolbar(self) -> QWidget: self._sync_btn.setIcon(_sync_ic) self._sync_btn.setIconSize(QSize((14), (14))) self._sync_btn.setToolTip( - "Apply per-feed settings: remove listened/old episodes, " - "fill empty slots with new episodes" + tr("Apply per-feed settings: remove listened/old episodes, fill empty slots with new episodes") ) self._sync_btn.clicked.connect(self._on_sync_podcasts) layout.addWidget(self._sync_btn) @@ -1481,7 +1505,7 @@ def _build_empty_page(self) -> QWidget: layout.addSpacing(12) heading = make_label( - "No Podcast Subscriptions", + tr("No Podcast Subscriptions"), size=(Metrics.FONT_PAGE_TITLE), weight=QFont.Weight.DemiBold, ) @@ -1491,8 +1515,10 @@ def _build_empty_page(self) -> QWidget: layout.addSpacing(6) desc = make_label( - "Search for podcasts or add an RSS feed to get started.\n" - "Episodes can be downloaded and synced to your iPod.", + tr( + "Search for podcasts or add an RSS feed to get started.\n" + "Episodes can be downloaded and synced to your iPod." + ), size=(Metrics.FONT_LG), style=LABEL_SECONDARY(), wrap=True, @@ -1502,7 +1528,7 @@ def _build_empty_page(self) -> QWidget: layout.addSpacing(16) - cta_btn = QPushButton("Add Your First Podcast") + cta_btn = QPushButton(tr("Add Your First Podcast")) cta_btn.setFont(QFont(FONT_FAMILY, (Metrics.FONT_MD), QFont.Weight.DemiBold)) cta_btn.setStyleSheet(accent_btn_css()) cta_btn.setFixedHeight(38) @@ -1619,7 +1645,7 @@ def _build_episode_panel(self) -> QWidget: info_col.setSpacing(4) self._feed_title_label = make_label( - "Select a podcast", + tr("Select a podcast"), size=Metrics.FONT_PAGE_TITLE, weight=QFont.Weight.DemiBold, ) @@ -1699,14 +1725,14 @@ def _build_episode_panel(self) -> QWidget: def _make_setting_combo(options: list[str], width: int = 110) -> QComboBox: cb = QComboBox() - cb.addItems(options) + _set_combo_options(cb, options) cb.setFixedWidth(width) cb.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) cb.setStyleSheet(_combo_style) return cb def _make_setting_label(text: str) -> QLabel: - lbl = QLabel(text) + lbl = QLabel(tr(text)) lbl.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) lbl.setStyleSheet(_lbl_css) return lbl @@ -1995,9 +2021,9 @@ def _on_feed_context_menu(self, pos): }} """) - refresh_action = menu.addAction("Refresh Feed") + refresh_action = menu.addAction(tr("Refresh Feed")) menu.addSeparator() - unsub_action = menu.addAction("Unsubscribe") + unsub_action = menu.addAction(tr("Unsubscribe")) action = menu.exec(self._feed_list.mapToGlobal(pos)) if action == refresh_action: @@ -2073,21 +2099,21 @@ def _on_episode_context_menu(self, pos) -> None: if can_add: n = len(can_add) suffix = f" ({n})" if n > 1 else "" - add_action = menu.addAction(f"Add to iPod{suffix}") + add_action = menu.addAction(f"{tr('Add to iPod')}{suffix}") if can_remove_dl: if add_action: menu.addSeparator() n = len(can_remove_dl) suffix = f" ({n})" if n > 1 else "" - remove_dl_action = menu.addAction(f"Remove Download{suffix}") + remove_dl_action = menu.addAction(f"{tr('Remove Download')}{suffix}") if can_remove_ipod: if add_action or remove_dl_action: menu.addSeparator() n = len(can_remove_ipod) suffix = f" ({n})" if n > 1 else "" - remove_ipod_action = menu.addAction(f"Remove from iPod{suffix}") + remove_ipod_action = menu.addAction(f"{tr('Remove from iPod')}{suffix}") viewport = self._episode_list.table.viewport() if not viewport: @@ -2180,7 +2206,7 @@ def _show_episodes(self, feed) -> None: if not feed: self._feed_header.show() - self._feed_title_label.setText("Select a podcast") + self._feed_title_label.setText(tr("Select a podcast")) self._feed_author_label.setText("") self._feed_description_label.setText("") self._feed_detail_label.setText("") @@ -2196,8 +2222,8 @@ def _show_episodes(self, feed) -> None: self._showing_combined_feed = False self._feed_header.show() - self._feed_title_label.setText(feed.title or "Untitled Podcast") - self._feed_author_label.setText(feed.author or "Unknown Author") + self._feed_title_label.setText(feed.title or tr("Untitled Podcast")) + self._feed_author_label.setText(feed.author or tr("Unknown Author")) desc_text = (feed.description or "").replace("\n", " ").strip() if len(desc_text) > 170: @@ -2209,14 +2235,20 @@ def _show_episodes(self, feed) -> None: detail_parts.append(feed.language.upper()) refreshed = _fmt_date(feed.last_refreshed) if refreshed: - detail_parts.append(f"Updated {refreshed}") + detail_parts.append(tr("Updated {date}").format(date=refreshed)) if feed.feed_url: - detail_parts.append("RSS feed linked") + detail_parts.append(tr("RSS feed linked")) self._feed_detail_label.setText(" · ".join(detail_parts)) - self._feed_stat_episodes.setText(f"Episodes: {len(feed.episodes)}") - self._feed_stat_downloaded.setText(f"Downloaded: {feed.downloaded_count}") - self._feed_stat_on_ipod.setText(f"On iPod: {feed.on_ipod_count}") + self._feed_stat_episodes.setText( + tr("Episodes: {count}").format(count=len(feed.episodes)) + ) + self._feed_stat_downloaded.setText( + tr("Downloaded: {count}").format(count=feed.downloaded_count) + ) + self._feed_stat_on_ipod.setText( + tr("On iPod: {count}").format(count=feed.on_ipod_count) + ) extra_parts = [] if feed.category: @@ -2335,7 +2367,16 @@ def _on_refresh_all(self) -> None: return self._refresh_btn.setEnabled(False) - self._set_status(f"Refreshing {len(feeds)} feeds…") + feed_count = len(feeds) + refresh_text = ( + tr("Refreshing {count} feed…").format(count=feed_count) + if feed_count == 1 + else tr("Refreshing {count} feeds…").format(count=feed_count) + ) + self._set_status( + refresh_text, + translate=False, + ) self._show_episode_loading( "Refreshing podcasts…", "Checking subscribed feeds for new episodes.", @@ -2380,7 +2421,15 @@ def _on_refresh_done(self, result) -> None: for f in self._store.get_feeds(): self._mark_feed_refreshed(f.feed_url) if count: - self._set_status(f"Refreshed {count} feed{'s' if count != 1 else ''}") + refreshed_text = ( + tr("Refreshed {count} feed").format(count=count) + if count == 1 + else tr("Refreshed {count} feeds").format(count=count) + ) + self._set_status( + refreshed_text, + translate=False, + ) # Reconcile episode statuses after RSS merge so that episodes # present on the iPod (but only known from RSS, not yet stored) @@ -2400,9 +2449,23 @@ def _on_refresh_done(self, result) -> None: if failures: if count: + failure_count = len(failures) + partial_text = ( + tr("Refreshed {count}; {failures} feed could not update").format( + count=count, + failures=failure_count, + ) + if failure_count == 1 + else tr( + "Refreshed {count}; {failures} feeds could not update" + ).format( + count=count, + failures=failure_count, + ) + ) self._set_status( - f"Refreshed {count}; {len(failures)} feed" - f"{'s' if len(failures) != 1 else ''} could not update" + partial_text, + translate=False, ) elif not self._episode_dicts: _feed_title, error = failures[0] @@ -2519,10 +2582,18 @@ def _on_sync_feeds_refreshed(self, refreshed_feeds: list) -> None: # Emit the plan (pending episodes will download during sync) summary_parts = [] if plan.to_remove: - summary_parts.append(f"{len(plan.to_remove)} to remove") + summary_parts.append( + tr("{count} to remove").format(count=len(plan.to_remove)) + ) if plan.to_add: - summary_parts.append(f"{len(plan.to_add)} to add") - self._set_status(f"Podcast sync: {', '.join(summary_parts)}") + summary_parts.append( + tr("{count} to add").format(count=len(plan.to_add)) + ) + summary_text = ", ".join(summary_parts) + self._set_status( + tr("Podcast sync: {summary}").format(summary=summary_text), + translate=False, + ) self._sync_btn.setEnabled(True) self.podcast_sync_requested.emit(plan) @@ -2601,7 +2672,10 @@ def _on_feed_fetched(self, feed) -> None: return self._store.add_feed(feed) self._mark_feed_refreshed(feed.feed_url) - self._set_status(f"Subscribed to {feed.title}") + self._set_status( + tr("Subscribed to {title}").format(title=feed.title), + translate=False, + ) self._showing_combined_feed = False self._refresh_feed_list() @@ -2628,14 +2702,20 @@ def _unsubscribe_feed(self, feed) -> None: if not self._store: return self._store.remove_feed(feed.feed_url) - self._set_status(f"Unsubscribed from {feed.title}") + self._set_status( + tr("Unsubscribed from {title}").format(title=feed.title), + translate=False, + ) self._selected_feed = None self._showing_combined_feed = False self._refresh_feed_list() def _refresh_single_feed(self, feed) -> None: """Refresh a single feed in the background.""" - self._set_status(f"Refreshing {feed.title}…") + self._set_status( + tr("Refreshing {title}…").format(title=feed.title), + translate=False, + ) self._show_episode_loading( "Refreshing this podcast…", "Checking the feed for the latest episodes.", @@ -2662,7 +2742,10 @@ def _on_single_feed_refreshed(self, feed) -> None: return self._store.update_feed(feed) self._mark_feed_refreshed(feed.feed_url) - self._set_status(f"Refreshed {feed.title}") + self._set_status( + tr("Refreshed {title}").format(title=feed.title), + translate=False, + ) was_combined = self._showing_combined_feed self._refresh_feed_list() @@ -2794,15 +2877,25 @@ def _build_and_emit_refs(self, actionable_refs) -> None: return n = len(plan.to_add) + send_text = ( + tr("Sending {count} episode to sync…").format(count=n) + if n == 1 + else tr("Sending {count} episodes to sync…").format(count=n) + ) self._set_action_status( - f"Sending {n} episode{'s' if n != 1 else ''} to sync…") + send_text, + translate=False, + ) self.podcast_sync_requested.emit(plan) def _on_add_error(self, error_tuple) -> None: self._progress_bar.hide() _, value, _ = error_tuple - self._set_action_status(f"Failed: {value}") + self._set_action_status( + tr("Failed: {error}").format(error=value), + translate=False, + ) # ── Remove download / Remove from iPod ─────────────────────────────── @@ -2845,7 +2938,15 @@ def _remove_download_refs(self, episode_refs: list) -> None: else: self._show_episodes(self._selected_feed) self._refresh_feed_list() - self._set_action_status(f"Removed {removed} download{'s' if removed != 1 else ''}") + removed_text = ( + tr("Removed {count} download").format(count=removed) + if removed == 1 + else tr("Removed {count} downloads").format(count=removed) + ) + self._set_action_status( + removed_text, + translate=False, + ) def _remove_from_ipod(self, episodes: list) -> None: """Build a sync plan to remove episodes from the iPod.""" @@ -2900,8 +3001,15 @@ def _remove_from_ipod_refs(self, episode_refs: list) -> None: return n = len(plan.to_remove) + removal_text = ( + tr("Sending {count} removal to sync…").format(count=n) + if n == 1 + else tr("Sending {count} removals to sync…").format(count=n) + ) self._set_action_status( - f"Sending {n} removal{'s' if n != 1 else ''} to sync\u2026") + removal_text, + translate=False, + ) self.podcast_sync_requested.emit(plan) def refresh_episodes(self) -> None: @@ -2944,18 +3052,16 @@ def _load_feed_settings(self, feed) -> None: self._feed_episode_slots.setValue(feed.episode_slots) _fill_display = {"newest": "Newest Episode", "next": "Next Episode"} - idx = self._feed_fill_mode.findText( + _set_combo_value( + self._feed_fill_mode, _fill_display.get(feed.fill_mode, "Newest Episode"), ) - if idx >= 0: - self._feed_fill_mode.setCurrentIndex(idx) _cl_display = {True: "Yes", False: "No"} - idx = self._feed_clear_listened.findText( + _set_combo_value( + self._feed_clear_listened, _cl_display.get(feed.clear_when_listened, "Yes"), ) - if idx >= 0: - self._feed_clear_listened.setCurrentIndex(idx) _older_display = { "immediate": "Immediately", @@ -2964,21 +3070,19 @@ def _load_feed_settings(self, feed) -> None: "1_month": "1 Month", "2_months": "2 Months", "3_months": "3 Months", "never": "Never", } - idx = self._feed_clear_older.findText( + _set_combo_value( + self._feed_clear_older, _older_display.get(feed.clear_older_than, "Never"), ) - if idx >= 0: - self._feed_clear_older.setCurrentIndex(idx) _method_display = { "remove": "Remove Immediately", "replace": "Mark for Replacement", } - idx = self._feed_clear_method.findText( + _set_combo_value( + self._feed_clear_method, _method_display.get(feed.clear_method, "Remove Immediately"), ) - if idx >= 0: - self._feed_clear_method.setCurrentIndex(idx) else: self._feed_episode_slots.setValue(3) self._feed_fill_mode.setCurrentIndex(0) @@ -3016,16 +3120,16 @@ def _on_feed_setting_changed(self, *_args) -> None: feed.episode_slots = self._feed_episode_slots.value() feed.fill_mode = _fill_keys.get( - self._feed_fill_mode.currentText(), "newest", + _combo_value(self._feed_fill_mode), "newest", ) feed.clear_when_listened = _cl_keys.get( - self._feed_clear_listened.currentText(), True, + _combo_value(self._feed_clear_listened), True, ) feed.clear_older_than = _older_keys.get( - self._feed_clear_older.currentText(), "never", + _combo_value(self._feed_clear_older), "never", ) feed.clear_method = _method_keys.get( - self._feed_clear_method.currentText(), "remove", + _combo_value(self._feed_clear_method), "remove", ) self._store.update_feed(feed) @@ -3306,26 +3410,40 @@ def _update_feed_list_icon(self, url: str, full_pm: QPixmap) -> None: # ── Status helpers ─────────────────────────────────────────────────── - def _set_status(self, text: str, timeout_ms: int = 5000) -> None: + def _set_status( + self, + text: str, + timeout_ms: int = 5000, + *, + translate: bool = True, + ) -> None: """Set toolbar status text with auto-clear.""" - self._status_label.setText(text) - if timeout_ms > 0 and text: - QTimer.singleShot(timeout_ms, lambda: self._clear_status_if(text)) + display_text = tr(text) if translate else text + self._status_label.setText(display_text) + if timeout_ms > 0 and display_text: + QTimer.singleShot(timeout_ms, lambda: self._clear_status_if(display_text)) def _clear_status_if(self, expected: str) -> None: """Clear status only if it still shows the expected message.""" if self._status_label.text() == expected: self._status_label.setText("") - def _set_action_status(self, text: str, timeout_ms: int = 5000) -> None: + def _set_action_status( + self, + text: str, + timeout_ms: int = 5000, + *, + translate: bool = True, + ) -> None: """Show the status toast with *text*, auto-hiding after *timeout_ms*.""" - self._action_status.setText(text) - if text: + display_text = tr(text) if translate else text + self._action_status.setText(display_text) + if display_text: self._status_toast.show() else: self._status_toast.hide() - if timeout_ms > 0 and text: - QTimer.singleShot(timeout_ms, lambda: self._clear_action_if(text)) + if timeout_ms > 0 and display_text: + QTimer.singleShot(timeout_ms, lambda: self._clear_action_if(display_text)) def _clear_action_if(self, expected: str) -> None: if self._action_status.text() == expected: diff --git a/GUI/widgets/podcastStates.py b/GUI/widgets/podcastStates.py index 00ffc07..0283ffe 100644 --- a/GUI/widgets/podcastStates.py +++ b/GUI/widgets/podcastStates.py @@ -14,6 +14,8 @@ QWidget, ) +from infrastructure.i18n import tr + from ..glyphs import glyph_pixmap from ..styles import ( FONT_FAMILY, @@ -118,16 +120,16 @@ def __init__(self, parent: QWidget | None = None, *, compact: bool = False): def show_loading(self, title: str, message: str) -> None: self._set_icon("broadcast", Colors.ACCENT) - self._title.setText(title) - self._set_message(message) + self._title.setText(tr(title)) + self._set_message(tr(message)) self._code.hide() self._progress.show() self._action.hide() def show_empty(self, title: str, message: str, *, glyph: str = "broadcast") -> None: self._set_icon(glyph, Colors.TEXT_TERTIARY) - self._title.setText(title) - self._set_message(message) + self._title.setText(tr(title)) + self._set_message(tr(message)) self._code.hide() self._progress.hide() self._action.hide() @@ -141,12 +143,12 @@ def show_error( action_text: str = "Try Again", ) -> None: self._set_icon("wifi-no-connection", Colors.WARNING) - self._title.setText(title) - self._set_message(message) + self._title.setText(tr(title)) + self._set_message(tr(message)) self._code.setText(code) self._code.setVisible(bool(code)) self._progress.hide() - self._action.setText(action_text) + self._action.setText(tr(action_text)) self._action.setVisible(bool(action_text)) def _set_message(self, message: str) -> None: diff --git a/GUI/widgets/settingsPage.py b/GUI/widgets/settingsPage.py index 554c069..8759ce0 100644 --- a/GUI/widgets/settingsPage.py +++ b/GUI/widgets/settingsPage.py @@ -34,6 +34,15 @@ QWidget, ) +from infrastructure.i18n import ( + language_display_name, + language_from_display_name, + language_options, + set_language, +) +from infrastructure.i18n import ( + tr as _, +) from infrastructure.settings_schema import ( BACKUP_BEFORE_SYNC_ASK, BACKUP_BEFORE_SYNC_AUTO, @@ -94,13 +103,13 @@ def __init__(self, title: str, description: str = ""): text_layout = QVBoxLayout() text_layout.setSpacing(3) - self.title_label = QLabel(title) + self.title_label = QLabel(_(title)) self.title_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG, QFont.Weight.DemiBold)) self.title_label.setStyleSheet(f"color: {Colors.TEXT_PRIMARY}; background: transparent; border: none;") text_layout.addWidget(self.title_label) if description: - self.desc_label = QLabel(description) + self.desc_label = QLabel(_(description)) self.desc_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.desc_label.setStyleSheet(f"color: {Colors.TEXT_TERTIARY}; background: transparent; border: none;") self.desc_label.setWordWrap(True) @@ -117,7 +126,7 @@ def add_control(self, widget: QWidget): def set_override_warning(self, visible: bool) -> None: """Show or hide a yellow 'overridden by device' warning under the title.""" if not hasattr(self, "_override_label"): - self._override_label = QLabel("Overridden by device settings") + self._override_label = QLabel(_("Overridden by device settings")) self._override_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self._override_label.setStyleSheet( f"color: {Colors.WARNING}; background: transparent; border: none;" @@ -186,18 +195,53 @@ def __init__(self, title: str, description: str = "", self.combo.setFixedWidth(130) self.combo.setFont(QFont(FONT_FAMILY, Metrics.FONT_MD)) self.combo.setStyleSheet(combo_css()) + self._options: list[str] = [] if options: - self.combo.addItems(options) - if current: - idx = self.combo.findText(current) - if idx >= 0: - self.combo.setCurrentIndex(idx) - self.combo.currentTextChanged.connect(self.changed.emit) + self.set_options(options, current=current) + self.combo.currentIndexChanged.connect(self._emit_current_value) self.add_control(self.combo) + def set_options(self, options: list[str], current: str = "") -> None: + """Replace dropdown choices while keeping raw English values stable.""" + + self._options = list(options) + self.combo.clear() + for option in self._options: + self.combo.addItem(_(option), option) + if current: + self.set_value(current) + + def _value_at(self, index: int) -> str: + if index < 0: + return "" + value = self.combo.itemData(index, Qt.ItemDataRole.UserRole) + if isinstance(value, str): + return value + return self.combo.itemText(index) + + def _emit_current_value(self, index: int) -> None: + self.changed.emit(self._value_at(index)) + + def find_value(self, value: str) -> int: + for index in range(self.combo.count()): + if self._value_at(index) == value: + return index + return -1 + + def set_value(self, value: str) -> None: + idx = self.find_value(value) + if idx < 0: + idx = self.combo.findText(value) + if idx >= 0: + self.combo.setCurrentIndex(idx) + @property def value(self) -> str: - return self.combo.currentText() + return self._value_at(self.combo.currentIndex()) + + @value.setter + def value(self, v: str): + self.set_value(v) class SpinRow(SettingRow): @@ -242,7 +286,7 @@ def __init__(self, title: str, description: str = "", path: str = "", super().__init__(title, description) self._resolve_default_fn = resolve_default_fn - self.open_btn = QPushButton("Open \u2197") + self.open_btn = QPushButton(_("Open \u2197")) self.open_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.open_btn.setStyleSheet(link_btn_css()) self.open_btn.setCursor(Qt.CursorShape.PointingHandCursor) @@ -252,14 +296,14 @@ def __init__(self, title: str, description: str = "", path: str = "", right_layout = QHBoxLayout() right_layout.setSpacing(8) - self.path_label = QLabel(self._truncate(path) if path else "Not set") + self.path_label = QLabel(self._truncate(path) if path else _("Not set")) self.path_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.path_label.setStyleSheet(f"color: {Colors.TEXT_SECONDARY}; background: transparent; border: none;") self.path_label.setMinimumWidth(120) self.path_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) right_layout.addWidget(self.path_label) - self.browse_btn = QPushButton("Browse…") + self.browse_btn = QPushButton(_("Browse…")) self.browse_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.browse_btn.setFixedWidth(80) self.browse_btn.setStyleSheet(btn_css( @@ -303,7 +347,7 @@ def _open_location(self): def _browse(self): folder = QFileDialog.getExistingDirectory( - self, "Select Folder", self._full_path, + self, _("Select Folder"), self._full_path, QFileDialog.Option.ShowDirsOnly, ) if folder: @@ -319,7 +363,7 @@ def value(self) -> str: @value.setter def value(self, v: str): self._full_path = v - self.path_label.setText(self._truncate(v) if v else "Not set") + self.path_label.setText(self._truncate(v) if v else _("Not set")) self._update_open_btn() @@ -335,7 +379,7 @@ def __init__(self, title: str, description: str = "", self._default_label = default_label self._resolve_default_fn = resolve_default_fn - self.open_btn = QPushButton("Open \u2197") + self.open_btn = QPushButton(_("Open \u2197")) self.open_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.open_btn.setStyleSheet(link_btn_css()) self.open_btn.setCursor(Qt.CursorShape.PointingHandCursor) @@ -345,7 +389,7 @@ def __init__(self, title: str, description: str = "", right_layout = QHBoxLayout() right_layout.setSpacing(8) - self.path_label = QLabel(self._truncate(path) if path else default_label) + self.path_label = QLabel(self._truncate(path) if path else _(default_label)) self.path_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.path_label.setStyleSheet( f"color: {Colors.TEXT_SECONDARY}; background: transparent; border: none;" @@ -356,7 +400,7 @@ def __init__(self, title: str, description: str = "", ) right_layout.addWidget(self.path_label) - self.browse_btn = QPushButton("Browse\u2026") + self.browse_btn = QPushButton(_("Browse…")) self.browse_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.browse_btn.setFixedWidth(80) self.browse_btn.setStyleSheet(btn_css( @@ -372,7 +416,7 @@ def __init__(self, title: str, description: str = "", self.clear_btn = QPushButton("\u2715") self.clear_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.clear_btn.setFixedWidth(28) - self.clear_btn.setToolTip("Reset to default") + self.clear_btn.setToolTip(_("Reset to default")) self.clear_btn.setStyleSheet(btn_css( bg="transparent", bg_hover=Colors.SURFACE_ACTIVE, @@ -417,7 +461,7 @@ def _open_location(self): def _browse(self): folder = QFileDialog.getExistingDirectory( - self, "Select Folder", self._full_path, + self, _("Select Folder"), self._full_path, QFileDialog.Option.ShowDirsOnly, ) if folder: @@ -428,7 +472,7 @@ def _browse(self): def _clear(self): self._full_path = "" - self.path_label.setText(self._default_label) + self.path_label.setText(_(self._default_label)) self._update_clear_visibility() self.changed.emit("") @@ -439,7 +483,7 @@ def value(self) -> str: @value.setter def value(self, v: str): self._full_path = v - self.path_label.setText(self._truncate(v) if v else self._default_label) + self.path_label.setText(self._truncate(v) if v else _(self._default_label)) self._update_clear_visibility() @@ -451,7 +495,7 @@ class ActionRow(SettingRow): def __init__(self, title: str, description: str = "", button_text: str = "Run"): super().__init__(title, description) - self.action_btn = QPushButton(button_text) + self.action_btn = QPushButton(_(button_text)) self.action_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.action_btn.setFixedWidth(100) self.action_btn.setCursor(Qt.CursorShape.PointingHandCursor) @@ -483,14 +527,14 @@ def __init__(self, title: str, description: str = "", path: str = "", right_layout = QHBoxLayout() right_layout.setSpacing(8) - self.path_label = QLabel(self._truncate(path) if path else "Auto-detect") + self.path_label = QLabel(self._truncate(path) if path else _("Auto-detect")) self.path_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.path_label.setStyleSheet(f"color: {Colors.TEXT_SECONDARY}; background: transparent; border: none;") self.path_label.setMinimumWidth(120) self.path_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) right_layout.addWidget(self.path_label) - self.browse_btn = QPushButton("Browse…") + self.browse_btn = QPushButton(_("Browse…")) self.browse_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.browse_btn.setFixedWidth(80) self.browse_btn.setStyleSheet(btn_css( @@ -506,7 +550,7 @@ def __init__(self, title: str, description: str = "", path: str = "", self.clear_btn = QPushButton("✕") self.clear_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.clear_btn.setFixedWidth(28) - self.clear_btn.setToolTip("Reset to auto-detect") + self.clear_btn.setToolTip(_("Reset to auto-detect")) self.clear_btn.setStyleSheet(btn_css( bg="transparent", bg_hover=Colors.SURFACE_ACTIVE, @@ -531,8 +575,8 @@ def _truncate(self, path: str) -> str: def _browse(self): start_dir = str(Path(self._full_path).parent) if self._full_path else "" - filepath, _ = QFileDialog.getOpenFileName( - self, "Select File", start_dir, self._filter_str, + filepath, _selected_filter = QFileDialog.getOpenFileName( + self, _("Select File"), start_dir, self._filter_str, ) if filepath: self._full_path = filepath @@ -541,7 +585,7 @@ def _browse(self): def _clear(self): self._full_path = "" - self.path_label.setText("Auto-detect") + self.path_label.setText(_("Auto-detect")) self.changed.emit("") @property @@ -551,7 +595,7 @@ def value(self) -> str: @value.setter def value(self, v: str): self._full_path = v - self.path_label.setText(self._truncate(v) if v else "Auto-detect") + self.path_label.setText(self._truncate(v) if v else _("Auto-detect")) class ToolRow(SettingRow): @@ -584,12 +628,12 @@ def __init__(self, title: str, description: str = ""): right_layout = QHBoxLayout() right_layout.setSpacing(8) - self.status_label = QLabel("Checking…") + self.status_label = QLabel(_("Checking…")) self.status_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.status_label.setStyleSheet(f"color: {Colors.TEXT_SECONDARY}; background: transparent; border: none;") right_layout.addWidget(self.status_label) - self.download_btn = QPushButton("Download") + self.download_btn = QPushButton(_("Download")) self.download_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.download_btn.setFixedWidth(90) self.download_btn.setCursor(Qt.CursorShape.PointingHandCursor) @@ -617,15 +661,15 @@ def set_status(self, found: bool, path: str = ""): self.status_label.setStyleSheet(f"color: {Colors.SUCCESS}; background: transparent; border: none;") self.download_btn.hide() else: - self.status_label.setText("Not found") + self.status_label.setText(_("Not found")) self.status_label.setStyleSheet(f"color: {Colors.WARNING}; background: transparent; border: none;") self.download_btn.show() def set_downloading(self): """Show downloading state.""" self.download_btn.setEnabled(False) - self.download_btn.setText("Downloading…") - self.status_label.setText("Downloading…") + self.download_btn.setText(_("Downloading…")) + self.status_label.setText(_("Downloading…")) self.status_label.setStyleSheet(f"color: {Colors.TEXT_SECONDARY}; background: transparent; border: none;") def set_lossy_encoder_statuses(self, statuses: dict[str, bool]): @@ -671,7 +715,7 @@ def __init__(self, title: str, description: str = "", link_url: str = ""): # Add a "Get token" link below the description if URL provided if link_url: - link_btn = QPushButton("Get token ↗") + link_btn = QPushButton(_("Get token ↗")) link_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) link_btn.setCursor(Qt.CursorShape.PointingHandCursor) link_btn.setStyleSheet(link_btn_css()) @@ -690,14 +734,14 @@ def __init__(self, title: str, description: str = "", link_url: str = ""): right_layout.addWidget(self.status_label) self.token_input = QLineEdit() - self.token_input.setPlaceholderText("Paste token here…") + self.token_input.setPlaceholderText(_("Paste token here…")) self.token_input.setFixedWidth(220) self.token_input.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.token_input.setEchoMode(QLineEdit.EchoMode.Password) self.token_input.setStyleSheet(input_css()) right_layout.addWidget(self.token_input) - self.save_btn = QPushButton("Connect") + self.save_btn = QPushButton(_("Connect")) self.save_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.save_btn.setFixedWidth(80) self.save_btn.setCursor(Qt.CursorShape.PointingHandCursor) @@ -715,7 +759,7 @@ def __init__(self, title: str, description: str = "", link_url: str = ""): self.clear_btn = QPushButton("✕") self.clear_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.clear_btn.setFixedWidth(28) - self.clear_btn.setToolTip("Disconnect") + self.clear_btn.setToolTip(_("Disconnect")) self.clear_btn.setStyleSheet(btn_css( bg="transparent", bg_hover=Colors.SURFACE_ACTIVE, @@ -734,7 +778,7 @@ def __init__(self, title: str, description: str = "", link_url: str = ""): def set_connected(self, username: str): """Show connected state with username.""" - self.status_label.setText(f"✓ Connected as {username}") + self.status_label.setText(f"✓ {_('Connected as')} {username}") self.status_label.setStyleSheet( f"color: {Colors.SUCCESS}; background: transparent; border: none;" ) @@ -749,7 +793,7 @@ def set_disconnected(self): self.token_input.show() self.save_btn.show() self.clear_btn.hide() - self.save_btn.setText("Connect") + self.save_btn.setText(_("Connect")) def set_error(self, message: str): """Show an error after validation fails.""" @@ -815,7 +859,7 @@ def __init__(self, title: str, description: str = "", link_url: str = ""): self._session_error_sig.connect(self._on_session_error) if link_url: - link_btn = QPushButton("Get API keys ↗") + link_btn = QPushButton(_("Get API keys ↗")) link_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) link_btn.setCursor(Qt.CursorShape.PointingHandCursor) link_btn.setStyleSheet(link_btn_css()) @@ -838,14 +882,14 @@ def __init__(self, title: str, description: str = "", link_url: str = ""): inputs_layout.setSpacing(8) self.api_key_input = QLineEdit() - self.api_key_input.setPlaceholderText("API Key") + self.api_key_input.setPlaceholderText(_("API Key")) self.api_key_input.setFixedWidth(160) self.api_key_input.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.api_key_input.setStyleSheet(input_css()) inputs_layout.addWidget(self.api_key_input) self.api_secret_input = QLineEdit() - self.api_secret_input.setPlaceholderText("API Secret") + self.api_secret_input.setPlaceholderText(_("API Secret")) self.api_secret_input.setFixedWidth(160) self.api_secret_input.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.api_secret_input.setEchoMode(QLineEdit.EchoMode.Password) @@ -854,7 +898,7 @@ def __init__(self, title: str, description: str = "", link_url: str = ""): right_layout.addWidget(self.inputs_widget) - self.connect_btn = QPushButton("Connect") + self.connect_btn = QPushButton(_("Connect")) self.connect_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.connect_btn.setFixedWidth(80) self.connect_btn.setCursor(Qt.CursorShape.PointingHandCursor) @@ -865,7 +909,7 @@ def __init__(self, title: str, description: str = "", link_url: str = ""): self.connect_btn.clicked.connect(self._start_auth_flow) right_layout.addWidget(self.connect_btn) - self.cancel_btn = QPushButton("Cancel") + self.cancel_btn = QPushButton(_("Cancel")) self.cancel_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.cancel_btn.setFixedWidth(80) self.cancel_btn.setCursor(Qt.CursorShape.PointingHandCursor) @@ -880,7 +924,7 @@ def __init__(self, title: str, description: str = "", link_url: str = ""): self.clear_btn = QPushButton("✕") self.clear_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.clear_btn.setFixedWidth(28) - self.clear_btn.setToolTip("Disconnect") + self.clear_btn.setToolTip(_("Disconnect")) self.clear_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.clear_btn.setStyleSheet(btn_css( bg="transparent", bg_hover=Colors.SURFACE_ACTIVE, bg_press=Colors.SURFACE_ALT, @@ -895,7 +939,7 @@ def __init__(self, title: str, description: str = "", link_url: str = ""): self.add_control(container) def set_connected(self, username: str): - self.status_label.setText(f"✓ Connected as {username}") + self.status_label.setText(f"✓ {_('Connected as')} {username}") self.status_label.setStyleSheet(f"color: {Colors.SUCCESS}; background: transparent; border: none;") self.inputs_widget.hide() self.connect_btn.hide() @@ -915,7 +959,7 @@ def set_disconnected(self, api_key: str = "", api_secret: str = ""): self.inputs_widget.show() self.connect_btn.show() self.connect_btn.setEnabled(True) - self.connect_btn.setText("Connect") + self.connect_btn.setText(_("Connect")) self.cancel_btn.hide() self.clear_btn.hide() self._polling_timer.stop() @@ -929,10 +973,10 @@ def _start_auth_flow(self): api_secret = self.api_secret_input.text().strip() if not api_key or not api_secret: - self.set_error("API Key and Secret required") + self.set_error(_("API Key and Secret required")) return - self.status_label.setText("Fetching token...") + self.status_label.setText(_("Fetching token...")) self.status_label.setStyleSheet(f"color: {Colors.TEXT_SECONDARY}; background: transparent; border: none;") self.api_key_input.setEnabled(False) self.api_secret_input.setEnabled(False) @@ -969,7 +1013,7 @@ def _on_token_fetched(self, token: str): auth_url = f"https://www.last.fm/api/auth/?api_key={api_key}&token={token}" QDesktopServices.openUrl(QUrl(auth_url)) - self.status_label.setText("Waiting for browser approval...") + self.status_label.setText(_("Waiting for browser approval...")) self.status_label.setStyleSheet(f"color: {Colors.ACCENT}; background: transparent; border: none;") self._is_polling = False self._polling_timer.start() @@ -1067,7 +1111,7 @@ def _on_session_error(self, err: str): def _cancel_auth(self): self._polling_timer.stop() self._is_polling = False - self.status_label.setText("Canceled") + self.status_label.setText(_("Canceled")) self.status_label.setStyleSheet(f"color: {Colors.TEXT_TERTIARY}; background: transparent; border: none;") self.cancel_btn.hide() self.connect_btn.show() @@ -1087,7 +1131,7 @@ class _CacheSizeRow(SettingRow): def __init__(self, settings_service: SettingsService): super().__init__("Cache Status", "Calculating…") self._settings_service = settings_service - self._clear_btn = QPushButton("Clear Cache") + self._clear_btn = QPushButton(_("Clear Cache")) self._clear_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self._clear_btn.setFixedWidth(110) self._clear_btn.setCursor(Qt.CursorShape.PointingHandCursor) @@ -1109,13 +1153,18 @@ def refresh(self) -> None: gb = stats["total_size_gb"] count = stats["total_files"] max_gb = stats.get("max_size_gb", 0.0) + file_noun = "file" if count == 1 else "files" if max_gb > 0: - self.desc_label.setText(f"{gb:.2f} GB used of {max_gb:.0f} GB · {count:,} files") + template = "{gb:.2f} GB used of {max_gb:.0f} GB · {count:,} " + file_noun + self.desc_label.setText( + _(template).format(gb=gb, max_gb=max_gb, count=count) + ) else: - self.desc_label.setText(f"{gb:.2f} GB · {count:,} files") + template = "{gb:.2f} GB · {count:,} " + file_noun + self.desc_label.setText(_(template).format(gb=gb, count=count)) self._clear_btn.setEnabled(count > 0) except Exception as exc: - self.desc_label.setText(f"Unavailable ({exc})") + self.desc_label.setText(_("Unavailable ({error})").format(error=exc)) def _on_clear(self) -> None: from PyQt6.QtWidgets import QMessageBox @@ -1123,9 +1172,9 @@ def _on_clear(self) -> None: from SyncEngine.transcode_cache import TranscodeCache reply = QMessageBox.question( self, - "Clear Transcode Cache", - "Delete all cached transcoded files?\n\n" - "They will be re-created on the next sync.", + _("Clear Transcode Cache"), + _("Delete all cached transcoded files?\n\n" + "They will be re-created on the next sync."), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, ) if reply != QMessageBox.StandardButton.Yes: @@ -1137,10 +1186,12 @@ def _on_clear(self) -> None: cache_dir, max_cache_size_gb=s.max_cache_size_gb, ).clear() - self.desc_label.setText(f"Cleared — {n:,} files removed") + noun = "file" if n == 1 else "files" + template = "Cleared — {count:,} " + noun + " removed" + self.desc_label.setText(_(template).format(count=n)) self._clear_btn.setEnabled(False) except Exception as exc: - self.desc_label.setText(f"Error clearing cache: {exc}") + self.desc_label.setText(_("Error clearing cache: {error}").format(error=exc)) class _SettingsCard(QFrame): @@ -1213,6 +1264,7 @@ class SettingsPage(QWidget): closed = pyqtSignal() # Emitted when user closes settings theme_changed = pyqtSignal() # Emitted when theme or contrast changes + language_changed = pyqtSignal() # Emitted when the interface language changes artwork_appearance_changed = pyqtSignal() # Emitted when artwork UI styling changes def __init__( @@ -1274,13 +1326,13 @@ def _build_sidebar(self) -> QFrame: back_btn = QPushButton("←") back_btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) back_btn.setCursor(Qt.CursorShape.PointingHandCursor) - back_btn.setToolTip("Back") + back_btn.setToolTip(_("Back")) back_btn.setStyleSheet(back_btn_css()) back_btn.clicked.connect(self._on_close) layout.addWidget(back_btn) # Title - title = QLabel("Settings") + title = QLabel(_("Settings")) title.setFont(QFont(FONT_FAMILY, Metrics.FONT_HERO, QFont.Weight.Bold)) title.setStyleSheet( f"color: {Colors.TEXT_PRIMARY}; background: transparent; border: none;" @@ -1297,7 +1349,7 @@ def _build_sidebar(self) -> QFrame: "External Tools", "Scrobbling", "Storage", "Backups", ] for i, name in enumerate(nav_items): - btn = QPushButton(name) + btn = QPushButton(_(name)) btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) btn.setCursor(Qt.CursorShape.PointingHandCursor) btn.clicked.connect(lambda _, idx=i: self._select_page(idx)) @@ -1323,8 +1375,8 @@ def _build_scope_switch(self) -> QFrame: lay.setContentsMargins(3, 3, 3, 3) lay.setSpacing(3) - self._scope_global_btn = QPushButton("Global") - self._scope_device_btn = QPushButton("Device") + self._scope_global_btn = QPushButton(_("Global")) + self._scope_device_btn = QPushButton(_("Device")) for btn in (self._scope_global_btn, self._scope_device_btn): btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM, QFont.Weight.DemiBold)) btn.setCursor(Qt.CursorShape.PointingHandCursor) @@ -1388,7 +1440,7 @@ def _make_page(self, title: str, *items) -> QScrollArea: layout.setSpacing(0) # Page title - title_label = QLabel(title) + title_label = QLabel(_(title)) title_label.setFont( QFont(FONT_FAMILY, Metrics.FONT_PAGE_TITLE, QFont.Weight.Bold) ) @@ -1400,7 +1452,7 @@ def _make_page(self, title: str, *items) -> QScrollArea: for item in items: if isinstance(item, str): - lbl = QLabel(item.upper()) + lbl = QLabel(_(item).upper()) lbl.setFont(QFont(FONT_FAMILY, Metrics.FONT_XS, QFont.Weight.Bold)) lbl.setStyleSheet( f"color: {Colors.TEXT_TERTIARY}; background: transparent;" @@ -1433,6 +1485,13 @@ def _build_general_page(self) -> QScrollArea: ) self.reset_device_settings.clicked.connect(self._reset_device_settings_to_global) + self.language_combo = ComboRow( + "Language", + "Choose the interface language. Changing it rebuilds the window.", + options=language_options(), + current=language_display_name("en"), + ) + self.theme_combo = ComboRow( "Theme", "Choose the color scheme for the interface. " @@ -1531,6 +1590,7 @@ def _build_general_page(self) -> QScrollArea: self.reset_device_settings, ) self._appearance_card = _SettingsCard( + self.language_combo, self.theme_combo, self.high_contrast, self.accent_color, @@ -1627,7 +1687,7 @@ def _build_sync_page(self) -> QScrollArea: def _build_transcoding_page(self) -> QScrollArea: self.lossy_encoder = ComboRow( "Lossy Encoder", - "Choose which lossy encoder to use." + "Choose which lossy encoder to use. " "Auto chooses the best available.", options=[ "Auto", @@ -1714,13 +1774,13 @@ def _build_transcoding_page(self) -> QScrollArea: ) self.smart_quality_by_type = ToggleRow( "Smart Quality by Content Type", - "Enable separate quality settings for podcasts and audiobooks." + "Enable separate quality settings for podcasts and audiobooks. " "Music tracks are unaffected.", ) self.normalize_sample_rate = ToggleRow( "Normalize to 44.1 kHz", "Always output audio at 44.1 kHz (CD rate). " - "Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC." + "Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC. " "When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower", ) @@ -1991,11 +2051,13 @@ def _sync_scope_availability(self) -> None: loading_device_settings = False if hasattr(self, "_scope_device_btn"): self._scope_device_btn.setEnabled(has_device) - self._scope_device_btn.setText("Device..." if loading_device_settings else "Device") + self._scope_device_btn.setText( + _("Device...") if loading_device_settings else _("Device") + ) self._scope_device_btn.setToolTip( - "Loading device settings..." + _("Loading device settings...") if loading_device_settings - else ("" if has_device else "Select an iPod to edit device settings") + else ("" if has_device else _("Select an iPod to edit device settings")) ) if not has_device and self._settings_scope == "device": self._settings_scope = "global" @@ -2090,6 +2152,7 @@ def _apply_scope_visibility(self) -> None: self._manage_card.setVisible(device_scope) self._set_section_visible("General", "Manage", device_scope) + self._appearance_card.set_row_visible(self.language_combo, not device_scope) self._appearance_card.set_row_visible(self.theme_combo, not device_scope) self._appearance_card.set_row_visible(self.high_contrast, not device_scope) self._appearance_card.set_row_visible(self.font_scale, not device_scope) @@ -2158,9 +2221,7 @@ def load_from_settings(self): "highest": "Highest", "lowest": "Lowest", "average": "Average", } rs_text = strategy_display.get(s.rating_conflict_strategy, "iPod Wins") - idx = self.rating_strategy.combo.findText(rs_text) - if idx >= 0: - self.rating_strategy.combo.setCurrentIndex(idx) + self.rating_strategy.value = rs_text # Scrobbling self.scrobble_on_sync.value = s.scrobble_on_sync @@ -2179,6 +2240,10 @@ def load_from_settings(self): self.rounded_artwork.value = s.rounded_artwork self.sharpen_artwork.value = s.sharpen_artwork + # Language + language_text = language_display_name(s.language) + self.language_combo.value = language_text + # Theme theme_display = { "dark": "Dark", "light": "Light", "system": "System", @@ -2188,16 +2253,12 @@ def load_from_settings(self): "catppuccin-latte": "Catppuccin Latte", } theme_text = theme_display.get(s.theme, "Dark") - idx = self.theme_combo.combo.findText(theme_text) - if idx >= 0: - self.theme_combo.combo.setCurrentIndex(idx) + self.theme_combo.value = theme_text # High contrast hc_display = {"off": "Off", "on": "On", "system": "System"} hc_text = hc_display.get(s.high_contrast, "Off") - idx = self.high_contrast.combo.findText(hc_text) - if idx >= 0: - self.high_contrast.combo.setCurrentIndex(idx) + self.high_contrast.value = hc_text # Accent color accent_display = { @@ -2207,23 +2268,17 @@ def load_from_settings(self): "pink": "Pink", } ac_text = accent_display.get(s.accent_color, "Blue (Default)") - idx = self.accent_color.combo.findText(ac_text) - if idx >= 0: - self.accent_color.combo.setCurrentIndex(idx) + self.accent_color.value = ac_text # Font scale - idx = self.font_scale.combo.findText(s.font_scale) - if idx >= 0: - self.font_scale.combo.setCurrentIndex(idx) + self.font_scale.value = s.font_scale self.transcode_cache_dir.value = s.transcode_cache_dir # Max cache size combo _size_map = {0.0: "Unlimited", 1.0: "1 GB", 2.0: "2 GB", 5.0: "5 GB", 10.0: "10 GB", 20.0: "20 GB", 50.0: "50 GB"} _size_text = _size_map.get(float(s.max_cache_size_gb), "5 GB") - idx = self.max_cache_size.combo.findText(_size_text) - if idx >= 0: - self.max_cache_size.combo.setCurrentIndex(idx) + self.max_cache_size.value = _size_text self.cache_status.refresh() self.settings_dir.value = s.settings_dir self.log_dir.value = s.log_dir @@ -2239,9 +2294,7 @@ def load_from_settings(self): backup_mode, _BACKUP_BEFORE_SYNC_DISPLAY[BACKUP_BEFORE_SYNC_AUTO], ) - idx = self.backup_before_sync.combo.findText(backup_mode_text) - if idx >= 0: - self.backup_before_sync.combo.setCurrentIndex(idx) + self.backup_before_sync.value = backup_mode_text # Refresh tool status indicators self._refresh_tool_status() @@ -2249,9 +2302,7 @@ def load_from_settings(self): # Max backups → combo text max_map = {0: "Unlimited", 5: "5", 10: "10", 20: "20"} mb_text = max_map.get(s.max_backups, "10") - idx = self.max_backups.combo.findText(mb_text) - if idx >= 0: - self.max_backups.combo.setCurrentIndex(idx) + self.max_backups.value = mb_text # Lossy encoder — also rebuilds bitrate_mode/vbr_level options for the encoder desired_enc = "Auto" if s.lossy_encoder == "auto" else s.lossy_encoder @@ -2261,36 +2312,26 @@ def load_from_settings(self): # Lossy quality (Auto mode) quality_display = {"high": "High Quality", "balanced": "Balanced", "compact": "Compact"} q_text = quality_display.get(s.lossy_quality, "Balanced") - idx = self.lossy_quality.combo.findText(q_text) - if idx >= 0: - self.lossy_quality.combo.setCurrentIndex(idx) + self.lossy_quality.value = q_text # Bitrate mode (manual encoder mode) bm_text = {"vbr": "VBR", "abr": "ABR", "cvbr": "CVBR"}.get(s.bitrate_mode, "CBR") - idx = self.bitrate_mode.combo.findText(bm_text) - if idx >= 0: - self.bitrate_mode.combo.setCurrentIndex(idx) + self.bitrate_mode.value = bm_text # Music CBR bitrate cbr_text = f"{s.music_lossy_cbr_bitrate} kbps" - idx = self.music_lossy_cbr_bitrate.combo.findText(cbr_text) - if idx >= 0: - self.music_lossy_cbr_bitrate.combo.setCurrentIndex(idx) + self.music_lossy_cbr_bitrate.value = cbr_text # VBR level vbr_text = self._vbr_level_to_text(selected_enc, s.vbr_level) - idx = self.vbr_level.combo.findText(vbr_text) - if idx >= 0: - self.vbr_level.combo.setCurrentIndex(idx) + self.vbr_level.value = vbr_text self._update_lossy_visibility() self._update_advanced_aac_visibility(selected_enc) # Spoken word bitrate spk_text = f"{s.spoken_lossy_cbr_bitrate} kbps" - idx = self.spoken_lossy_cbr_bitrate.combo.findText(spk_text) - if idx >= 0: - self.spoken_lossy_cbr_bitrate.combo.setCurrentIndex(idx) + self.spoken_lossy_cbr_bitrate.value = spk_text # Prefer lossy toggle self.prefer_lossy.value = s.prefer_lossy @@ -2306,9 +2347,7 @@ def load_from_settings(self): cutoff_display = {0: "Auto", 15000: "15 kHz", 16000: "16 kHz", 17000: "17 kHz", 18000: "18 kHz", 19000: "19 kHz", 20000: "20 kHz"} c_text = cutoff_display.get(s.aac_cutoff, "Auto") - idx = self.aac_cutoff.combo.findText(c_text) - if idx >= 0: - self.aac_cutoff.combo.setCurrentIndex(idx) + self.aac_cutoff.value = c_text self.fdk_afterburner.value = s.fdk_afterburner self.aac_tns.value = s.aac_tns self.aac_pns.value = s.aac_pns @@ -2320,27 +2359,19 @@ def load_from_settings(self): # Video CRF → combo text crf_map = {18: "18 (High)", 20: "20 (Good)", 23: "23 (Balanced)", 26: "26 (Low)", 28: "28 (Very Low)"} crf_text = crf_map.get(s.video_crf, "23 (Balanced)") - idx = self.video_crf.combo.findText(crf_text) - if idx >= 0: - self.video_crf.combo.setCurrentIndex(idx) + self.video_crf.value = crf_text # Video preset → combo text - idx = self.video_preset.combo.findText(s.video_preset) - if idx >= 0: - self.video_preset.combo.setCurrentIndex(idx) + self.video_preset.value = s.video_preset # Sync workers → combo text workers_map = {0: "Auto", 1: "1", 2: "2", 4: "4", 6: "6", 8: "8"} sw_text = workers_map.get(s.sync_workers, "Auto") - idx = self.sync_workers.combo.findText(sw_text) - if idx >= 0: - self.sync_workers.combo.setCurrentIndex(idx) + self.sync_workers.value = sw_text write_workers_map = {0: "Auto", 1: "1", 2: "2", 4: "4"} dww_text = write_workers_map.get(s.device_write_workers, "Auto") - idx = self.device_write_workers.combo.findText(dww_text) - if idx >= 0: - self.device_write_workers.combo.setCurrentIndex(idx) + self.device_write_workers.value = dww_text self._apply_scope_visibility() @@ -2381,6 +2412,7 @@ def load_from_settings(self): self.show_art.changed.connect(self._save) self.rounded_artwork.changed.connect(self._save) self.sharpen_artwork.changed.connect(self._save) + self.language_combo.changed.connect(self._save) self.accent_color.changed.connect(self._save) self.theme_combo.changed.connect(self._save) self.high_contrast.changed.connect(self._save) @@ -2428,12 +2460,9 @@ def _refresh_encoder_options(self, desired: str = "Auto") -> str: combo = self.lossy_encoder.combo combo.blockSignals(True) - combo.clear() - combo.addItems(options) - idx = combo.findText(desired) if desired in options else 0 - combo.setCurrentIndex(idx) + self.lossy_encoder.set_options(options, current=desired if desired in options else options[0]) combo.blockSignals(False) - return options[idx] + return self.lossy_encoder.value def _update_encoder_dependent_combos(self, encoder_text: str) -> None: """Repopulate bitrate_mode and vbr_level combos to match the selected encoder.""" @@ -2445,36 +2474,35 @@ def _update_encoder_dependent_combos(self, encoder_text: str) -> None: bm_options = ["CBR"] bm_combo = self.bitrate_mode.combo - current_bm = bm_combo.currentText() + current_bm = self.bitrate_mode.value bm_combo.blockSignals(True) - bm_combo.clear() - bm_combo.addItems(bm_options) - idx = bm_combo.findText(current_bm) - bm_combo.setCurrentIndex(max(0, idx)) + self.bitrate_mode.set_options(bm_options, current=current_bm) + if self.bitrate_mode.find_value(current_bm) < 0: + self.bitrate_mode.combo.setCurrentIndex(0) bm_combo.blockSignals(False) vbr_combo = self.vbr_level.combo - current_vbr = vbr_combo.currentText() + current_vbr = self.vbr_level.value vbr_combo.blockSignals(True) - vbr_combo.clear() if encoder_text == "libfdk_aac": - vbr_combo.addItems([ + vbr_options = [ "VBR 1 (Low)", "VBR 2", "VBR 3", "VBR 4", "VBR 5 (High)", - ]) + ] default_idx = 3 # VBR 4 elif encoder_text == "aac_at": - vbr_combo.addItems([ + vbr_options = [ "q0 (Best)", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "q9", "q10", "q11", "q12", "q13", "q14 (Lowest)", - ]) + ] default_idx = 9 # q9 mid-range else: - vbr_combo.addItems([ + vbr_options = [ "q0 (Best)", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "q9 (Smallest)", - ]) + ] default_idx = 4 # q4 - idx = vbr_combo.findText(current_vbr) - vbr_combo.setCurrentIndex(idx if idx >= 0 else default_idx) + self.vbr_level.set_options(vbr_options, current=current_vbr) + if self.vbr_level.find_value(current_vbr) < 0: + vbr_combo.setCurrentIndex(default_idx) vbr_combo.blockSignals(False) def _update_lossy_visibility(self) -> None: @@ -2597,6 +2625,7 @@ def _read_controls_into_settings(self, s, include_global_only: bool) -> None: s.show_art_in_tracklist = self.show_art.value if include_global_only: + s.language = language_from_display_name(self.language_combo.value) s.rounded_artwork = self.rounded_artwork.value s.sharpen_artwork = self.sharpen_artwork.value # Theme @@ -2706,6 +2735,12 @@ def _apply_theme_change_if_needed(self, before) -> None: Metrics.apply_font_scale(s.font_scale) self.theme_changed.emit() + def _apply_language_change_if_needed(self, before: str) -> None: + language = self._settings_service.get_effective_settings().language + if language != before: + set_language(language) + self.language_changed.emit() + def _reset_device_settings_to_global(self) -> None: """Reset the selected iPod settings file from current global settings.""" if self._device_settings_pending: @@ -2748,6 +2783,7 @@ def _save(self, *_args): effective_before.accent_color, effective_before.font_scale, ) + language_before = effective_before.language ctx = self._current_device_context() if self._settings_scope == "device" else None if ctx: @@ -2796,6 +2832,7 @@ def _save(self, *_args): pass self._apply_theme_change_if_needed(theme_before) + self._apply_language_change_if_needed(language_before) if artwork_before != ( effective_after.show_art_in_tracklist, effective_after.rounded_artwork, @@ -2837,22 +2874,25 @@ def _check_for_updates(self): from GUI.auto_updater import UpdateChecker, UpdateResult self.version_row.action_btn.setEnabled(False) - self.version_row.action_btn.setText("Checking…") + self.version_row.action_btn.setText(_("Checking…")) self._update_checker = UpdateChecker(self) def _on_result(result: UpdateResult): self.version_row.action_btn.setEnabled(True) - self.version_row.action_btn.setText("Check") + self.version_row.action_btn.setText(_("Check")) if result.error: - QMessageBox.warning(self, "Update Check Failed", result.error) + QMessageBox.warning(self, _("Update Check Failed"), result.error) return if not result.update_available: QMessageBox.information( - self, "Up to Date", - f"You are running the latest version (v{result.current_version}).", + self, + _("Up to Date"), + _("You are running the latest version (v{version}).").format( + version=result.current_version + ), ) return @@ -2904,18 +2944,21 @@ def _handle_update_result(self, result): if not result.download_url: QMessageBox.information( - self, "No Binary Available", - "No pre-built binary was found for your platform.\n\n" - f"Visit {result.release_page} to download manually.", + self, + _("No Binary Available"), + _( + "No pre-built binary was found for your platform.\n\n" + "Visit {release_page} to download manually." + ).format(release_page=result.release_page), ) QDesktopServices.openUrl(QUrl(result.release_page)) return # Start download with progress dialog progress = QProgressDialog( - "Downloading update…", "Cancel", 0, 100, self, + _("Downloading update…"), _("Cancel"), 0, 100, self, ) - progress.setWindowTitle("iOpenPod Update") + progress.setWindowTitle(_("iOpenPod Update")) progress.setMinimumDuration(0) progress.setAutoClose(False) progress.setAutoReset(False) @@ -3014,7 +3057,7 @@ def _refresh_tool_status(self): ffprobe = find_ffprobe(settings.ffmpeg_path) if ffmpeg else None self.ffmpeg_tool.set_status(bool(ffmpeg and ffprobe), ffmpeg or "") if ffmpeg and not ffprobe: - self.ffmpeg_tool.status_label.setText("ffprobe missing") + self.ffmpeg_tool.status_label.setText(_("ffprobe missing")) aac_enc = available_aac_encoders(settings.ffmpeg_path) if ffmpeg else set() mp3_enc = available_mp3_encoders(settings.ffmpeg_path) if ffmpeg else set() self.ffmpeg_tool.set_lossy_encoder_statuses( @@ -3069,14 +3112,14 @@ def _on_ffmpeg_downloaded(self): """Called on main thread after FFmpeg download completes.""" self._refresh_tool_status() self.ffmpeg_tool.download_btn.setEnabled(True) - self.ffmpeg_tool.download_btn.setText("Download") + self.ffmpeg_tool.download_btn.setText(_("Download")) @pyqtSlot() def _on_fpcalc_downloaded(self): """Called on main thread after fpcalc download completes.""" self._refresh_tool_status() self.fpcalc_tool.download_btn.setEnabled(True) - self.fpcalc_tool.download_btn.setText("Download") + self.fpcalc_tool.download_btn.setText(_("Download")) # ── ListenBrainz Scrobbling handlers ─────────────────────────────────── @@ -3134,7 +3177,7 @@ def _on_listenbrainz_token_changed(self, token: str): # Validate the token in a background thread self.listenbrainz_token_row.save_btn.setEnabled(False) - self.listenbrainz_token_row.save_btn.setText("Validating…") + self.listenbrainz_token_row.save_btn.setText(_("Validating…")) import threading @@ -3165,10 +3208,10 @@ def _on_listenbrainz_validate_result(self): pending = self._pending_lb_result token, username, scope, root, key, use_global = pending self.listenbrainz_token_row.save_btn.setEnabled(True) - self.listenbrainz_token_row.save_btn.setText("Connect") + self.listenbrainz_token_row.save_btn.setText(_("Connect")) if not username: - self.listenbrainz_token_row.set_error("Invalid token") + self.listenbrainz_token_row.set_error(_("Invalid token")) return self._save_listenbrainz_credentials( diff --git a/GUI/widgets/sidebar.py b/GUI/widgets/sidebar.py index e4b85aa..7ecb27a 100644 --- a/GUI/widgets/sidebar.py +++ b/GUI/widgets/sidebar.py @@ -3,6 +3,7 @@ from PyQt6.QtWidgets import QFrame, QHBoxLayout, QLabel, QLineEdit, QProgressBar, QPushButton, QSizePolicy, QVBoxLayout, QWidget from app_core.device_identity import format_checksum_type_name +from infrastructure.i18n import tr as _ from ..glyphs import glyph_icon, glyph_pixmap from ..ipod_images import get_ipod_image @@ -30,6 +31,9 @@ # so only printable Unicode is allowed (no control characters). _MAX_IPOD_NAME_LEN = 63 _IPOD_NAME_RE = QRegularExpression(r"^[^\x00-\x1f\x7f]*$") +_COMPACT_ACTION_MIN_FONT = 8 +_COMPACT_ACTION_TEXT_GUARD = 6 +_COMPACT_ACTION_ICON_GAP = 6 def _dash(value) -> str: @@ -37,7 +41,7 @@ def _dash(value) -> str: def _yes_no(value) -> str: - return "Yes" if bool(value) else "No" + return _("Yes") if bool(value) else _("No") def _hex_id(value: int, width: int = 4) -> str: @@ -62,6 +66,64 @@ def _format_format_ids(formats: dict[int, tuple[int, int]]) -> str: return ", ".join(str(fid) for fid in sorted(formats)) +class _CompactActionButton(QPushButton): + """Sidebar half-width action button that protects translated labels.""" + + def __init__(self, text: str): + super().__init__() + self._full_text = text + self._base_font = QFont(FONT_FAMILY, Metrics.FONT_LG, QFont.Weight.DemiBold) + self.setMinimumWidth(0) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.setToolTip(text) + self.setFont(self._base_font) + super().setText(text) + QTimer.singleShot(0, self.refit_text) + + def set_compact_text(self, text: str) -> None: + self._full_text = text + self.setToolTip(text) + super().setText(text) + self.refit_text() + + def resizeEvent(self, event) -> None: # type: ignore[override] + super().resizeEvent(event) + self.refit_text() + + def refit_text(self) -> None: + """Fit the full label into the current button width when possible.""" + if not self._full_text: + return + + available = self.contentsRect().width() - _COMPACT_ACTION_TEXT_GUARD + if not self.icon().isNull(): + available -= self.iconSize().width() + _COMPACT_ACTION_ICON_GAP + if available <= 0: + return + + base_size = self._base_font.pointSize() + for size in range(base_size, _COMPACT_ACTION_MIN_FONT - 1, -1): + font = QFont(self._base_font) + font.setPointSize(size) + metrics = QFontMetrics(font) + if metrics.horizontalAdvance(self._full_text) <= available: + self.setFont(font) + super().setText(self._full_text) + return + + font = QFont(self._base_font) + font.setPointSize(_COMPACT_ACTION_MIN_FONT) + metrics = QFontMetrics(font) + self.setFont(font) + super().setText( + metrics.elidedText( + self._full_text, + Qt.TextElideMode.ElideRight, + available, + ) + ) + + class _RenameLineEdit(QLineEdit): """QLineEdit that emits cancelled on Escape.""" @@ -118,7 +180,7 @@ def __init__(self, label: str, value: str = ""): layout.setContentsMargins(0, (3), 0, (3)) layout.setSpacing(6) - self.label_widget = QLabel(label) + self.label_widget = QLabel(_(label)) self.label_widget.setFont(QFont(FONT_FAMILY, Metrics.FONT_XS)) self.label_widget.setStyleSheet(LABEL_TERTIARY()) self.label_widget.setSizePolicy( @@ -185,15 +247,15 @@ def __init__(self): name_layout.setSpacing(1) self._name_layout = name_layout - self.name_label = QLabel("No Device") + self.name_label = QLabel(_("No Device")) self.name_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_XXL, QFont.Weight.Bold)) self.name_label.setStyleSheet(LABEL_PRIMARY()) self.name_label.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) - self.name_label.setToolTip("Click to rename your iPod") + self.name_label.setToolTip(_("Click to rename your iPod")) self.name_label.mousePressEvent = lambda ev: self._start_rename() name_layout.addWidget(self.name_label) - self.model_label = QLabel("Press Select to choose your iPod") + self.model_label = QLabel(_("Press Select to choose your iPod")) self.model_label.setFont(QFont(FONT_FAMILY, Metrics.FONT_SM)) self.model_label.setStyleSheet(LABEL_SECONDARY()) self.model_label.setWordWrap(True) @@ -213,7 +275,7 @@ def __init__(self): self.eject_button = QPushButton() self.eject_button.setFixedSize((26), (26)) self.eject_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) - self.eject_button.setToolTip("Safely eject the iPod from your system") + self.eject_button.setToolTip(_("Safely eject the iPod from your system")) self.eject_button.setStyleSheet(btn_css( bg=Colors.SURFACE, bg_hover=Colors.SURFACE_HOVER, @@ -271,8 +333,8 @@ def __init__(self): inv_layout.setContentsMargins(0, (2), 0, (2)) inv_layout.setSpacing(10) - self._inv_songs = _InventoryCell("—", "Songs") - self._inv_hours = _InventoryCell("—", "Hours") + self._inv_songs = _InventoryCell("—", _("Songs")) + self._inv_hours = _InventoryCell("—", _("Hours")) inv_layout.addWidget(self._inv_songs, 1) inv_layout.addWidget(self._inv_hours, 1) @@ -280,7 +342,7 @@ def __init__(self): layout.addWidget(inv_widget) # Technical details section (collapsible) - self.tech_toggle = QPushButton("Technical Details") + self.tech_toggle = QPushButton(_("Technical Details")) _chev = glyph_icon("chevron-right", (12), Colors.TEXT_TERTIARY) if _chev: self.tech_toggle.setIcon(_chev) @@ -360,7 +422,7 @@ def __init__(self): self.chapter_formats_row = TechInfoRow("Chapter Img:", "—") # Three grouped sections with hairline separators - tech_layout.addWidget(make_section_header("Identity")) + tech_layout.addWidget(make_section_header(_("Identity"))) for w in ( self.model_num_row, self.serial_row, self.firmware_row, self.board_row, self.family_id_row, self.updater_family_id_row, @@ -370,7 +432,7 @@ def __init__(self): tech_layout.addWidget(w) tech_layout.addWidget(make_separator()) - tech_layout.addWidget(make_section_header("USB / SCSI")) + tech_layout.addWidget(make_section_header(_("USB / SCSI"))) for w in ( self.usb_vid_row, self.usb_pid_row, self.usb_serial_row, self.scsi_row, self.bus_format_row, self.usbstor_row, @@ -379,7 +441,7 @@ def __init__(self): tech_layout.addWidget(w) tech_layout.addWidget(make_separator()) - tech_layout.addWidget(make_section_header("Database")) + tech_layout.addWidget(make_section_header(_("Database"))) for w in ( self.db_version_row, self.device_db_version_row, self.shadow_db_version_row, self.sqlite_row, self.db_id_row, @@ -388,7 +450,7 @@ def __init__(self): tech_layout.addWidget(w) tech_layout.addWidget(make_separator()) - tech_layout.addWidget(make_section_header("Capabilities")) + tech_layout.addWidget(make_section_header(_("Capabilities"))) for w in ( self.podcast_support_row, self.voice_memo_row, self.sparse_art_row, self.max_transfer_row, @@ -397,7 +459,7 @@ def __init__(self): tech_layout.addWidget(w) tech_layout.addWidget(make_separator()) - tech_layout.addWidget(make_section_header("Storage")) + tech_layout.addWidget(make_section_header(_("Storage"))) for w in ( self.disk_size_row, self.free_space_row, self.art_formats_row, self.photo_formats_row, self.chapter_formats_row, @@ -433,7 +495,7 @@ def _set_default_icon(self) -> None: def _start_rename(self, event=None): """Show an inline QLineEdit to rename the iPod.""" current = self.name_label.text() - if current == "No Device" or self._rename_edit is not None: + if current == _("No Device") or self._rename_edit is not None: return self._rename_edit = _RenameLineEdit(current) @@ -510,11 +572,11 @@ def _fit_name_font(self, text: str): def update_device_info(self, name: str, model: str = "", device_info=None): """Update device name and model.""" self._device_info = device_info - display = name or "No Device" + display = name or _("No Device") self.name_label.setText(display) self._fit_name_font(display) self.model_label.setText(model) - self.eject_button.setEnabled(bool(name) and display != "No Device") + self.eject_button.setEnabled(bool(name) and display != _("No Device")) # Try to load real product photo from centralized store family = "" @@ -542,7 +604,7 @@ def update_device_info(self, name: str, model: str = "", device_info=None): def source_tip(field: str, value: str) -> str: source = field_sources.get(field, "") if source and value != "—": - return f"{value}\nSource: {source}" + return _("{value}\nSource: {source}").format(value=value, source=source) return value self.model_num_row.setValue(dev.model_number or '—') @@ -569,7 +631,7 @@ def source_tip(field: str, value: str) -> str: ) or str(conflicts) self.conflicts_row.setValue(str(len(conflicts)), detail) else: - self.conflicts_row.setValue("None") + self.conflicts_row.setValue(_("None")) usb_vid = _hex_id(dev.usb_vid) self.usb_vid_row.setValue(usb_vid, source_tip("usb_vid", usb_vid)) @@ -607,7 +669,7 @@ def source_tip(field: str, value: str) -> str: self.id_method_row.setValue(dev.identification_method or '—') self.checksum_row.setValue(format_checksum_type_name(dev.checksum_type)) - scheme_names = {-1: '—', 0: 'None', 1: 'Scheme 1', 2: 'Scheme 2'} + scheme_names = {-1: "—", 0: _("None"), 1: _("Scheme 1"), 2: _("Scheme 2")} self.hash_scheme_row.setValue( scheme_names.get(dev.hashing_scheme, str(dev.hashing_scheme)) ) @@ -671,10 +733,16 @@ def source_tip(field: str, value: str) -> str: used_pct = int(((dev.disk_size_gb - dev.free_space_gb) / dev.disk_size_gb) * 100) self.storage_bar.setValue(max(0, min(100, used_pct))) self._capacity_label.setText( - f"{dev.free_space_gb:.1f} GB free of {dev.disk_size_gb:.1f} GB" + _("{free:.1f} GB free of {total:.1f} GB").format( + free=dev.free_space_gb, + total=dev.disk_size_gb, + ) ) self._capacity_widget.setToolTip( - f"{dev.free_space_gb:.1f} GB free of {dev.disk_size_gb:.1f} GB" + _("{free:.1f} GB free of {total:.1f} GB").format( + free=dev.free_space_gb, + total=dev.disk_size_gb, + ) ) self._capacity_widget.show() @@ -751,29 +819,30 @@ def show_save_indicator(self, state: str) -> None: self._save_label.setStyleSheet( f"background: transparent; border: none; color: {Colors.TEXT_TERTIARY};" ) - self._save_label.setText("Saving…") + self._save_label.setText(_("Saving…")) self._save_label.show() elif state == "saved": self._save_label.setStyleSheet( f"background: transparent; border: none; color: {Colors.SUCCESS};" ) - self._save_label.setText("✓ Saved") + self._save_label.setText(f"✓ {_('Saved')}") self._save_label.show() self._save_hide_timer.start(2500) elif state == "error": self._save_label.setStyleSheet( f"background: transparent; border: none; color: {Colors.DANGER};" ) - self._save_label.setText("⚠ Save failed") + self._save_label.setText(f"⚠ {_('Save failed')}") self._save_label.show() self._save_hide_timer.start(4000) def clear(self): """Clear all info (when no device selected).""" self._device_info = None - self.name_label.setText("No Device") - self._fit_name_font("No Device") - self.model_label.setText("Press Select to choose your iPod") + no_device = _("No Device") + self.name_label.setText(no_device) + self._fit_name_font(no_device) + self.model_label.setText(_("Press Select to choose your iPod")) self._set_default_icon() self._capacity_label.setText("—") self._capacity_widget.hide() @@ -861,13 +930,11 @@ def __init__(self): self.deviceSelectLayout.setContentsMargins(0, 0, 0, 0) self.deviceSelectLayout.setSpacing(6) - self.deviceButton = QPushButton("Select") - self.rescanButton = QPushButton("Rescan") + self.deviceButton = _CompactActionButton(_("Select")) + self.rescanButton = _CompactActionButton(_("Rescan")) self.deviceButton.setStyleSheet(toolbar_btn_css()) self.rescanButton.setStyleSheet(toolbar_btn_css()) - self.deviceButton.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG, QFont.Weight.DemiBold)) - self.rescanButton.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG, QFont.Weight.DemiBold)) _icon_sz = QSize((20), (20)) _bi = glyph_icon("tablet", (20), Colors.TEXT_SECONDARY) @@ -879,13 +946,16 @@ def __init__(self): self.rescanButton.setIcon(_bi) self.rescanButton.setIconSize(_icon_sz) - self.deviceSelectLayout.addWidget(self.deviceButton) - self.deviceSelectLayout.addWidget(self.rescanButton) + self.deviceButton.refit_text() + self.rescanButton.refit_text() + + self.deviceSelectLayout.addWidget(self.deviceButton, 1) + self.deviceSelectLayout.addWidget(self.rescanButton, 1) self.sidebarLayout.addLayout(self.deviceSelectLayout) # Sync button - row 2 (full width) - self.syncButton = QPushButton("Sync with PC") + self.syncButton = QPushButton(_("Sync with PC")) self.syncButton.setStyleSheet(accent_btn_css()) self.syncButton.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG, QFont.Weight.DemiBold)) _bi = glyph_icon("download", (20), Colors.TEXT_ON_ACCENT) @@ -895,7 +965,7 @@ def __init__(self): self.sidebarLayout.addWidget(self.syncButton) # Backup button - self.backupButton = QPushButton("Backups") + self.backupButton = QPushButton(_("Backups")) self.backupButton.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) self.backupButton.setStyleSheet(sidebar_nav_css()) _bi = glyph_icon("archive", (20), Colors.TEXT_SECONDARY) @@ -904,10 +974,10 @@ def __init__(self): self.backupButton.setIconSize(_icon_sz) self.sidebarLayout.addWidget(self.backupButton) - self.tagFixButton = QPushButton("Normalize Tags") + self.tagFixButton = QPushButton(_("Normalize Tags")) self.tagFixButton.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) self.tagFixButton.setStyleSheet(sidebar_nav_css()) - self.tagFixButton.setToolTip("Preview and apply iPod-friendly tag fixes across the whole library.") + self.tagFixButton.setToolTip(_("Preview and apply iPod-friendly tag fixes across the whole library.")) _bi = glyph_icon("check-circle", (20), Colors.TEXT_SECONDARY) if _bi: self.tagFixButton.setIcon(_bi) @@ -924,7 +994,7 @@ def __init__(self): library_layout.setContentsMargins(0, 0, 0, 0) library_layout.setSpacing(1) - lib_label = make_section_header("Library") + lib_label = make_section_header(_("Library")) lib_label.setStyleSheet(lib_label.styleSheet() + f" padding-left: {(4)}px;") library_layout.addWidget(lib_label) @@ -942,7 +1012,7 @@ def __init__(self): _nav_icon_sz = QSize((20), (20)) for category, icon_name in self.category_glyphs.items(): - btn = QPushButton(category) + btn = QPushButton(_(category)) btn.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) icon = glyph_icon(icon_name, (20), Colors.TEXT_SECONDARY) if icon: @@ -966,7 +1036,7 @@ def __init__(self): self.sidebarLayout.addWidget(make_separator()) # Settings button at bottom - self.settingsButton = QPushButton("Settings") + self.settingsButton = QPushButton(_("Settings")) self.settingsButton.setFont(QFont(FONT_FAMILY, Metrics.FONT_LG)) self.settingsButton.setStyleSheet(btn_css( bg="transparent", diff --git a/GUI/widgets/trackListTitleBar.py b/GUI/widgets/trackListTitleBar.py index 3370ac0..e6031a8 100644 --- a/GUI/widgets/trackListTitleBar.py +++ b/GUI/widgets/trackListTitleBar.py @@ -2,6 +2,8 @@ from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QFrame, QHBoxLayout, QLabel, QPushButton, QWidget +from infrastructure.i18n import tr as _ + from ..glyphs import glyph_icon from ..styles import ( FONT_FAMILY, @@ -136,16 +138,16 @@ def __init__(self, splitterToControl): self.setMaximumHeight(40) self.setFixedHeight(40) - self.title = QLabel("Tracks") + self.title = QLabel(_("Tracks")) self.title.setFont(QFont(FONT_FAMILY, Metrics.FONT_TITLE, QFont.Weight.Bold)) self.button1 = QPushButton() self._icon_size = QSize(18, 18) - self.button1.setToolTip("Minimize") + self.button1.setToolTip(_("Minimize")) self.button1.clicked.connect(self._toggleMinimize) self.button2 = QPushButton() - self.button2.setToolTip("Maximize") + self.button2.setToolTip(_("Maximize")) self.button2.clicked.connect(self._toggleMaximize) self.titleBarLayout.addWidget(self.title) @@ -155,9 +157,9 @@ def __init__(self, splitterToControl): self.resetColor() - def setTitle(self, title: str): + def setTitle(self, title: str, *, translate: bool = False): """Set the title text.""" - self.title.setText(title) + self.title.setText(_(title) if translate else title) def setColor(self, r: int, g: int, b: int, text: tuple | None = None, text_secondary: tuple | None = None): diff --git a/README.md b/README.md index 3513010..89d8768 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,9 @@ Useful contributions include: - Focused pull requests for documented issues - Joining the discord to coordinate +For user-facing UI text and language updates, follow the +[internationalization maintenance guide](docs/i18n.md). + Please open an issue before starting major changes, or use the [Discord server](https://discord.gg/9Yy499Tf5d) to discuss implementation details. ### Related Projects diff --git a/app_core/bootstrap.py b/app_core/bootstrap.py index 0d07061..8b488f3 100644 --- a/app_core/bootstrap.py +++ b/app_core/bootstrap.py @@ -190,6 +190,7 @@ def run_pyqt_app(context: AppContext | None = None) -> None: build_palette, resolve_accent_color, ) + from infrastructure.i18n import set_language QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough @@ -200,6 +201,7 @@ def run_pyqt_app(context: AppContext | None = None) -> None: app.setStyle(DarkScrollbarStyle("Fusion")) settings_snapshot = context.settings.get_effective_snapshot() + set_language(settings_snapshot.language) Colors.apply_theme( settings_snapshot.theme, settings_snapshot.high_contrast, diff --git a/app_core/jobs.py b/app_core/jobs.py index f8d317d..b483151 100644 --- a/app_core/jobs.py +++ b/app_core/jobs.py @@ -19,6 +19,7 @@ from PyQt6.QtCore import QThread, pyqtSignal +from infrastructure.i18n import tr as _ from infrastructure.media_folders import media_folder_paths from .dropped_files import ( @@ -66,22 +67,26 @@ def tool_names(self) -> tuple[str, ...]: @property def tool_list(self) -> str: - return " and ".join(self.tool_names) + return _(" and ").join(self.tool_names) @property def install_help_text(self) -> str: lines = [] if self.missing_fpcalc: lines.append( - "fpcalc is required for sync.\n" - "Install from: https://acoustid.org/chromaprint" + _( + "fpcalc is required for sync.\n" + "Install from: https://acoustid.org/chromaprint" + ) ) if self.missing_ffmpeg: lines.append( - "FFmpeg and ffprobe are required for transcoding and media probing.\n" - "Install from: https://ffmpeg.org" + _( + "FFmpeg and ffprobe are required for transcoding and media probing.\n" + "Install from: https://ffmpeg.org" + ) ) - lines.append("You can also set custom paths in\nSettings -> External Tools.") + lines.append(_("You can also set custom paths in\nSettings -> External Tools.")) return "\n\n".join(lines) @@ -1914,11 +1919,11 @@ def run(self) -> None: try: if not self._ipod_path: _reload_after_itunesdb_write(self._cache) - self.failed.emit("No iPod connected.") + self.failed.emit(_("No iPod connected.")) return if not self._cache.get_data(): _reload_after_itunesdb_write(self._cache) - self.failed.emit("No iPod database loaded.") + self.failed.emit(_("No iPod database loaded.")) return tracks_data, playlists_data, artwork_sources = ( @@ -1932,7 +1937,7 @@ def run(self) -> None: ) if not result.success: _reload_after_itunesdb_write(self._cache) - self.failed.emit(result.error or "Database write failed.") + self.failed.emit(result.error or _("Database write failed.")) return _delete_imported_otg_files(self._ipod_path) @@ -1963,11 +1968,11 @@ def run(self) -> None: try: if not self._ipod_path: _reload_after_itunesdb_write(self._cache) - self.failed.emit("No iPod connected.") + self.failed.emit(_("No iPod connected.")) return if not self._cache.get_data(): _reload_after_itunesdb_write(self._cache) - self.failed.emit("No iPod database loaded.") + self.failed.emit(_("No iPod database loaded.")) return tracks_data, playlists_data, artwork_sources = ( @@ -1981,7 +1986,7 @@ def run(self) -> None: ) if not result.success: _reload_after_itunesdb_write(self._cache) - self.failed.emit(result.error or "Database write failed.") + self.failed.emit(result.error or _("Database write failed.")) return _reload_after_itunesdb_write(self._cache) @@ -2257,7 +2262,7 @@ def _on_sync_progress(progress) -> None: ) if not write_result.success: _reload_after_itunesdb_write(self._cache) - self.failed.emit(write_result.error or "Database write failed.") + self.failed.emit(write_result.error or _("Database write failed.")) return _reload_after_itunesdb_write(self._cache) diff --git a/app_core/services.py b/app_core/services.py index b065f2e..89046d5 100644 --- a/app_core/services.py +++ b/app_core/services.py @@ -73,6 +73,7 @@ class SettingsSnapshot: rounded_artwork: bool sharpen_artwork: bool track_list_columns_by_content: dict[str, dict[str, int]] + language: str theme: str high_contrast: str font_scale: str diff --git a/docs/i18n.md b/docs/i18n.md new file mode 100644 index 0000000..0bf191f --- /dev/null +++ b/docs/i18n.md @@ -0,0 +1,24 @@ +# Internationalization + +iOpenPod uses gettext catalogs for UI text. Source code keeps English strings +as the canonical msgids and wraps user-visible literals with `tr(...)` or the +`tr as _` alias. + +## Workflow + +1. Wrap new UI strings in `_(...)`. +2. Run `uv run python scripts/update_translations.py` to append missing msgids + to `locale//LC_MESSAGES/iopenpod.po`. +3. Fill in `msgstr` values in the `.po` file. +4. Run `uv run python scripts/compile_translations.py` to regenerate `.mo` + files used at runtime. +5. Run `uv run python scripts/update_translations.py --check` before shipping + to confirm catalogs are not behind source. + +Do not add translation dictionaries to Python modules. Keep translations in +`locale/*/LC_MESSAGES/iopenpod.po`, and commit the compiled `.mo` files so the +installed wheel can load translations without requiring gettext tooling. + +Changing language in Settings saves `AppSettings.language`, updates the process +gettext catalog, and rebuilds the window so existing widgets receive translated +labels. diff --git a/infrastructure/i18n.py b/infrastructure/i18n.py new file mode 100644 index 0000000..9b02f2e --- /dev/null +++ b/infrastructure/i18n.py @@ -0,0 +1,116 @@ +"""Application internationalization backed by gettext catalogs.""" + +from __future__ import annotations + +import gettext +from pathlib import Path + +LANGUAGE_EN = "en" +LANGUAGE_ZH = "zh" +LANGUAGE_DE = "de" +LANGUAGE_FR = "fr" +LANGUAGE_ES = "es" +SUPPORTED_LANGUAGES = frozenset({ + LANGUAGE_EN, + LANGUAGE_ZH, + LANGUAGE_DE, + LANGUAGE_FR, + LANGUAGE_ES, +}) + +LANGUAGE_DISPLAY_NAMES = { + LANGUAGE_EN: "English", + LANGUAGE_ZH: "中文", + LANGUAGE_DE: "Deutsch", + LANGUAGE_FR: "Français", + LANGUAGE_ES: "Español", +} + +DOMAIN = "iopenpod" +LOCALE_DIR = Path(__file__).resolve().parents[1] / "locale" + +_current_language = LANGUAGE_EN +_translation: gettext.NullTranslations = gettext.NullTranslations() + + +def normalize_language(language: object) -> str: + """Return a supported language code, defaulting to English.""" + + if isinstance(language, str): + normalized = language.strip().lower().replace("_", "-") + if normalized in {"zh", "zh-cn", "zh-hans", "chinese", "中文"}: + return LANGUAGE_ZH + if normalized in {"en", "en-us", "en-gb", "english"}: + return LANGUAGE_EN + if normalized in {"de", "de-de", "german", "deutsch"}: + return LANGUAGE_DE + if normalized in {"fr", "fr-fr", "french", "français", "francais"}: + return LANGUAGE_FR + if normalized in {"es", "es-es", "spanish", "español", "espanol"}: + return LANGUAGE_ES + return LANGUAGE_EN + + +def _gettext_language(language: str) -> str: + return { + LANGUAGE_ZH: "zh_CN", + LANGUAGE_DE: "de", + LANGUAGE_FR: "fr", + LANGUAGE_ES: "es", + }.get(language, "en") + + +def set_language(language: object) -> str: + """Set the process-local UI language and return the normalized code.""" + + global _current_language, _translation + _current_language = normalize_language(language) + if _current_language == LANGUAGE_EN: + _translation = gettext.NullTranslations() + else: + _translation = gettext.translation( + DOMAIN, + localedir=LOCALE_DIR, + languages=[_gettext_language(_current_language)], + fallback=True, + ) + return _current_language + + +def get_language() -> str: + """Return the active process-local UI language code.""" + + return _current_language + + +def language_display_name(language: object) -> str: + """Return the selector label for a language code.""" + + return LANGUAGE_DISPLAY_NAMES[normalize_language(language)] + + +def language_from_display_name(display_name: str) -> str: + """Return a language code from a selector label.""" + + for code, label in LANGUAGE_DISPLAY_NAMES.items(): + if display_name == label: + return code + return normalize_language(display_name) + + +def language_options() -> list[str]: + """Return language selector options in a stable order.""" + + return [ + LANGUAGE_DISPLAY_NAMES[LANGUAGE_EN], + LANGUAGE_DISPLAY_NAMES[LANGUAGE_ZH], + LANGUAGE_DISPLAY_NAMES[LANGUAGE_DE], + LANGUAGE_DISPLAY_NAMES[LANGUAGE_FR], + LANGUAGE_DISPLAY_NAMES[LANGUAGE_ES], + ] + + +def tr(text: str) -> str: + """Translate an English source string for the current UI language.""" + + return _translation.gettext(text) diff --git a/infrastructure/settings_persistence.py b/infrastructure/settings_persistence.py index 067f28c..20d7121 100644 --- a/infrastructure/settings_persistence.py +++ b/infrastructure/settings_persistence.py @@ -6,6 +6,7 @@ import os from dataclasses import asdict +from .i18n import normalize_language from .media_folders import media_folder_entries_to_settings from .settings_paths import ( default_settings_dir, @@ -23,6 +24,7 @@ def save_app_settings(settings: AppSettings) -> None: """Write settings to the active settings directory.""" + settings.language = normalize_language(settings.language) apply_backup_before_sync_mode(settings) active_dir = settings.settings_dir or default_settings_dir() os.makedirs(active_dir, exist_ok=True) @@ -93,6 +95,7 @@ def load_app_settings() -> AppSettings: settings.backup_before_sync = ( settings.backup_before_sync_mode == BACKUP_BEFORE_SYNC_AUTO ) + settings.language = normalize_language(settings.language) except (json.JSONDecodeError, UnicodeDecodeError, OSError): pass diff --git a/infrastructure/settings_schema.py b/infrastructure/settings_schema.py index 210b0f5..dc086cd 100644 --- a/infrastructure/settings_schema.py +++ b/infrastructure/settings_schema.py @@ -5,6 +5,8 @@ from dataclasses import dataclass, field from typing import Any +from .i18n import LANGUAGE_EN, normalize_language + BACKUP_BEFORE_SYNC_AUTO = "auto" BACKUP_BEFORE_SYNC_ASK = "ask" BACKUP_BEFORE_SYNC_OFF = "off" @@ -159,6 +161,7 @@ class AppSettings: rounded_artwork: bool = False sharpen_artwork: bool = True track_list_columns_by_content: dict[str, dict[str, int]] = field(default_factory=dict) + language: str = LANGUAGE_EN theme: str = "dark" high_contrast: str = "off" font_scale: str = "100%" @@ -181,6 +184,7 @@ class AppSettings: max_backups: int = 10 def __post_init__(self) -> None: + self.language = normalize_language(self.language) apply_backup_before_sync_mode(self) diff --git a/locale/de/LC_MESSAGES/iopenpod.mo b/locale/de/LC_MESSAGES/iopenpod.mo new file mode 100644 index 0000000..7c9e0ff Binary files /dev/null and b/locale/de/LC_MESSAGES/iopenpod.mo differ diff --git a/locale/de/LC_MESSAGES/iopenpod.po b/locale/de/LC_MESSAGES/iopenpod.po new file mode 100644 index 0000000..4b19cdc --- /dev/null +++ b/locale/de/LC_MESSAGES/iopenpod.po @@ -0,0 +1,3025 @@ +#: GUI/widgets/photoBrowser.py:457 GUI/widgets/podcastBrowser.py:2330 +msgid "" +msgstr "" +"Project-Id-Version: iOpenPod\n" +"Report-Msgid-Bugs-To: \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "" +"A crash report has been saved to:\n" +msgstr "" +"Ein Absturzbericht wurde gespeichert unter:\n" + +#: GUI/widgets/settingsPage.py:885 +msgid "API Key" +msgstr "API-Schlüssel" + +#: GUI/widgets/settingsPage.py:976 +msgid "API Key and Secret required" +msgstr "API-Schlüssel und Secret erforderlich" + +#: GUI/widgets/settingsPage.py:892 +msgid "API Secret" +msgstr "API-Secret" + +#: GUI/widgets/musicBrowser.py:336 +msgid "All Tracks" +msgstr "Alle Titel" + +#: GUI/widgets/settingsPage.py:1608 +msgid "About" +msgstr "Über" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Accent Color" +msgstr "Akzentfarbe" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Advanced AAC" +msgstr "Erweitertes AAC" + +#: GUI/widgets/photoBrowser.py:439 +msgid "Albums" +msgstr "Alben" + +msgid "Album" +msgstr "Album" + +msgid "" +"An unexpected error occurred:\n" +"\n" +msgstr "" +"Ein unerwarteter Fehler ist aufgetreten:\n" +"\n" + +#: GUI/widgets/settingsPage.py:1608 +msgid "Appearance" +msgstr "Darstellung" + +#: GUI/widgets/settingsPage.py:1546 +msgid "Apply a subtle display-only sharpening pass to album art in grid cards and track lists. This does not modify artwork written to your iPod." +msgstr "Apply a subtle display-only sharpening pass to album art in grid cards and track lists. This does not modify artwork written to your iPod." + +msgid "Artists" +msgstr "Künstler" + +msgid "Artist" +msgstr "Künstler" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Audio" +msgstr "Audio" + +msgid "Audiobooks" +msgstr "Hörbücher" + +#: GUI/widgets/settingsPage.py:588 GUI/widgets/settingsPage.py:530 GUI/widgets/settingsPage.py:598 +msgid "Auto-detect" +msgstr "Automatisch erkennen" + +msgid "Ask Each Time" +msgstr "Jedes Mal fragen" + +#: GUI/widgets/settingsPage.py:1329 +msgid "Back" +msgstr "Zurück" + +msgid "Backup Folder" +msgstr "Backup-Ordner" + +#: GUI/widgets/settingsPage.py:2023 GUI/widgets/sidebar.py:906 +msgid "Backups" +msgstr "Backups" + +msgid "Backups and Rollback" +msgstr "Backups und Wiederherstellung" + +#: GUI/widgets/settingsPage.py:1787 +msgid "Bandwidth Cutoff" +msgstr "Bandbreiten-Grenze" + +msgid "Before Sync" +msgstr "Vor der Synchronisierung" + +#: GUI/widgets/settingsPage.py:1676 +msgid "Behavior" +msgstr "Verhalten" + +msgid "Duration" +msgstr "Dauer" + +#: GUI/widgets/settingsPage.py:1709 +msgid "Bitrate Mode" +msgstr "Bitratenmodus" + +#: GUI/widgets/settingsPage.py:1507 +msgid "Boost text and border contrast for accessibility. System follows your OS accessibility setting." +msgstr "Boost text and border contrast for accessibility. System follows your OS accessibility setting." + +msgid "Browse..." +msgstr "Durchsuchen..." + +#: GUI/widgets/settingsPage.py:306 GUI/widgets/settingsPage.py:403 GUI/widgets/settingsPage.py:537 +msgid "Browse…" +msgstr "Durchsuchen…" + +msgid "Cache Status" +msgstr "Cache-Status" + +msgid "Calculating…" +msgstr "Wird berechnet…" + +#: GUI/app.py:2187 GUI/widgets/settingsPage.py:912 GUI/widgets/settingsPage.py:2959 +msgid "Cancel" +msgstr "Abbrechen" + +#: GUI/widgets/settingsPage.py:1114 +msgid "Canceled" +msgstr "Abgebrochen" + +#: GUI/widgets/settingsPage.py:1554 GUI/widgets/settingsPage.py:2883 +msgid "Check" +msgstr "Prüfen" + +#: GUI/widgets/settingsPage.py:1554 +msgid "Check for a newer version of iOpenPod." +msgstr "Check for a newer version of iOpenPod." + +#: GUI/widgets/settingsPage.py:631 GUI/widgets/settingsPage.py:2877 +msgid "Checking…" +msgstr "Wird geprüft…" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Choose the color scheme for the interface. System follows your OS preference." +msgstr "Choose the color scheme for the interface. System follows your OS preference." + +#: GUI/widgets/settingsPage.py:1488 +msgid "Choose the interface language. Changing it rebuilds the window." +msgstr "Choose the interface language. Changing it rebuilds the window." + +#: GUI/widgets/settingsPage.py:1134 +msgid "Clear Cache" +msgstr "Cache leeren" + +#: GUI/widgets/settingsPage.py:1175 +msgid "Clear Transcode Cache" +msgstr "Transcode-Cache leeren" + +#: GUI/widgets/sidebar.py:193 +msgid "Click to rename your iPod" +msgstr "Klicken, um deinen iPod umzubenennen" + +#: GUI/widgets/settingsPage.py:1640 +msgid "Compute Sound Check" +msgstr "Sound Check berechnen" + +#: GUI/widgets/settingsPage.py:744 GUI/widgets/settingsPage.py:796 GUI/widgets/settingsPage.py:901 GUI/widgets/settingsPage.py:962 GUI/widgets/settingsPage.py:3211 +msgid "Connect" +msgstr "Verbinden" + +#: GUI/widgets/settingsPage.py:781 GUI/widgets/settingsPage.py:942 +msgid "Connected as" +msgstr "Verbunden als" + +#: GUI/widgets/settingsPage.py:1746 +msgid "Convert WAV to ALAC" +msgstr "WAV in ALAC umwandeln" + +#: GUI/widgets/settingsPage.py:1481 +msgid "Copy the current global values into this iPod's settings file." +msgstr "Copy the current global values into this iPod's settings file." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Customize the accent color used throughout the interface. Match iPod uses the body color of your connected iPod." +msgstr "Customize the accent color used throughout the interface. Match iPod uses the body color of your connected iPod." + +#: GUI/widgets/settingsPage.py:1176 +msgid "" +"Delete all cached transcoded files?\n" +"\n" +"They will be re-created on the next sync." +msgstr "" +"Alle zwischengespeicherten transcodierten Dateien löschen?\n" +"\n" +"Sie werden bei der nächsten Synchronisierung neu erstellt." + +#: GUI/widgets/settingsPage.py:1379 GUI/widgets/settingsPage.py:2055 +msgid "Device" +msgstr "Gerät" + +#: GUI/widgets/settingsPage.py:2055 +msgid "Device..." +msgstr "Gerät..." + +#: GUI/widgets/settingsPage.py:762 GUI/widgets/settingsPage.py:927 +msgid "Disconnect" +msgstr "Trennen" + +#: GUI/app.py:2589 GUI/widgets/settingsPage.py:636 GUI/widgets/settingsPage.py:3115 GUI/widgets/settingsPage.py:3122 +msgid "Download" +msgstr "Herunterladen" + +#: GUI/widgets/settingsPage.py:671 GUI/widgets/settingsPage.py:672 +msgid "Downloading…" +msgstr "Wird heruntergeladen…" + +#: GUI/widgets/settingsPage.py:1893 +msgid "External Tools" +msgstr "Externe Werkzeuge" + +#: GUI/widgets/settingsPage.py:1869 +msgid "FFmpeg" +msgstr "FFmpeg" + +#: GUI/widgets/settingsPage.py:1882 +msgid "FFmpeg Path Override" +msgstr "FFmpeg-Pfad überschreiben" + +#: GUI/widgets/settingsPage.py:979 +msgid "Fetching token..." +msgstr "Token wird abgerufen..." + +#: GUI/widgets/settingsPage.py:1653 +msgid "Fit Thumbnails" +msgstr "Miniaturen einpassen" + +#: GUI/widgets/settingsPage.py:1515 +msgid "Font Size" +msgstr "Schriftgröße" + +#: GUI/widgets/settingsPage.py:1608 GUI/widgets/settingsPage.py:1928 +msgid "General" +msgstr "Allgemein" + +msgid "Genres" +msgstr "Genres" + +#: GUI/widgets/settingsPage.py:862 +msgid "Get API keys ↗" +msgstr "API-Schlüssel abrufen ↗" + +#: GUI/widgets/settingsPage.py:718 +msgid "Get token ↗" +msgstr "Token abrufen ↗" + +#: GUI/widgets/settingsPage.py:1378 +msgid "Global" +msgstr "Global" + +#: GUI/widgets/sidebar.py:276 +msgid "Hours" +msgstr "Stunden" + +#: GUI/widgets/settingsPage.py:1475 +msgid "Ignore this iPod's settings file and use the PC settings while this iPod is selected." +msgstr "Ignore this iPod's settings file and use the PC settings while this iPod is selected." + +#: GUI/widgets/settingsPage.py:1507 +msgid "Increased Contrast" +msgstr "Erhöhter Kontrast" + +#: GUI/widgets/settingsPage.py:1819 +msgid "Intensity Stereo" +msgstr "Intensity Stereo" + +#: GUI/app.py:627 GUI/app.py:611 +msgid "Invalid iPod Folder" +msgstr "Ungültiger iPod-Ordner" + +#: GUI/widgets/settingsPage.py:1488 +msgid "Language" +msgstr "Sprache" + +#: GUI/widgets/settingsPage.py:1918 +msgid "Last.fm" +msgstr "Last.fm" + +#: GUI/widgets/sidebar.py:935 +msgid "Library" +msgstr "Mediathek" + +msgid "LIBRARY" +msgstr "MEDIATHEK" + +msgid "Library (Master)" +msgstr "Mediathek (Master)" + +#: GUI/widgets/settingsPage.py:1908 +msgid "ListenBrainz" +msgstr "ListenBrainz" + +#: GUI/widgets/settingsPage.py:2058 +msgid "Loading device settings..." +msgstr "Geräteeinstellungen werden geladen..." + +#: GUI/app.py:482 +msgid "Loading iPod..." +msgstr "iPod wird geladen..." + +msgid "Log Folder" +msgstr "Log-Ordner" + +#: GUI/widgets/settingsPage.py:1688 +msgid "Lossy Encoder" +msgstr "Verlustbehafteter Encoder" + +#: GUI/widgets/settingsPage.py:1608 +msgid "Manage" +msgstr "Verwalten" + +msgid "Maximum Backups" +msgstr "Maximale Backups" + +#: GUI/widgets/settingsPage.py:1813 +msgid "Mid/Side Stereo" +msgstr "Mid/Side-Stereo" + +#: GUI/widgets/settingsPage.py:1769 +msgid "Mono for Spoken Word" +msgstr "Mono für Sprache" + +msgid "Movies" +msgstr "Filme" + +#: GUI/widgets/settingsPage.py:1716 +msgid "Music Bitrate" +msgstr "Musik-Bitrate" + +#: GUI/widgets/settingsPage.py:1702 +msgid "Music Quality" +msgstr "Musikqualität" + +msgid "Music Videos" +msgstr "Musikvideos" + +#: GUI/widgets/podcastBrowser.py:1767 GUI/widgets/sidebar.py:41 +msgid "No" +msgstr "Nein" + +#: GUI/app.py:1418 GUI/app.py:1575 GUI/app.py:2087 GUI/widgets/sidebar.py:781 GUI/widgets/sidebar.py:189 GUI/widgets/sidebar.py:514 GUI/widgets/sidebar.py:437 GUI/widgets/sidebar.py:518 +msgid "No Device" +msgstr "Kein Gerät" + +#: GUI/app.py:441 +msgid "" +"No device is currently selected.\n" +"Choose an iPod to access your library and sync tools." +msgstr "" +"Aktuell ist kein Gerät ausgewählt.\n" +"Wähle einen iPod, um auf Mediathek und Synchronisierung zuzugreifen." + +#: GUI/widgets/sidebar.py:611 GUI/widgets/sidebar.py:573 +msgid "None" +msgstr "Keine" + +msgid "Name" +msgstr "Name" + +#: GUI/widgets/sidebar.py:915 +msgid "Normalize Tags" +msgstr "Tags normalisieren" + +#: GUI/widgets/settingsPage.py:1780 +msgid "Normalize to 44.1 kHz" +msgstr "Auf 44,1 kHz normalisieren" + +#: GUI/widgets/settingsPage.py:664 +msgid "Not found" +msgstr "Nicht gefunden" + +#: GUI/widgets/settingsPage.py:299 GUI/widgets/settingsPage.py:366 +msgid "Not set" +msgstr "Nicht festgelegt" + +#: GUI/widgets/settingsPage.py:1561 +msgid "Open" +msgstr "Öffnen" + +#: GUI/widgets/settingsPage.py:1561 +msgid "Open the GitHub issue tracker to report problems or request features." +msgstr "Open the GitHub issue tracker to report problems or request features." + +#: GUI/widgets/settingsPage.py:289 GUI/widgets/settingsPage.py:382 +msgid "Open ↗" +msgstr "Öffnen ↗" + +#: GUI/widgets/settingsPage.py:129 +msgid "Overridden by device settings" +msgstr "Durch Geräteeinstellungen überschrieben" + +#: GUI/widgets/settingsPage.py:1619 +msgid "Parallel Workers" +msgstr "Parallele Aufgaben" + +#: GUI/widgets/settingsPage.py:1627 +msgid "Parallel Writes" +msgstr "Parallele Schreibvorgänge" + +#: GUI/widgets/settingsPage.py:737 +msgid "Paste token here…" +msgstr "Token hier einfügen…" + +#: GUI/widgets/settingsPage.py:1807 +msgid "Perceptual Noise Substitution (PNS)" +msgstr "Perceptual Noise Substitution (PNS)" + +#: GUI/widgets/settingsPage.py:1676 +msgid "Performance" +msgstr "Leistung" + +msgid "Photos" +msgstr "Fotos" + +#: GUI/widgets/settingsPage.py:1943 GUI/widgets/settingsPage.py:1961 GUI/widgets/settingsPage.py:1968 +msgid "Platform default" +msgstr "Plattformstandard" + +#: GUI/widgets/playlistBrowser.py:1353 +msgid "Playlists" +msgstr "Wiedergabelisten" + +msgid "PODCAST PLAYLISTS" +msgstr "PODCAST-WIEDERGABELISTEN" + +msgid "Please report this issue on GitHub." +msgstr "Bitte melde dieses Problem auf GitHub." + +msgid "Podcasts" +msgstr "Podcasts" + +#: GUI/widgets/settingsPage.py:1735 +msgid "Prefer Lossy Encoding" +msgstr "Verlustbehaftete Codierung bevorzugen" + +#: GUI/widgets/sidebar.py:197 GUI/widgets/sidebar.py:784 +msgid "Press Select to choose your iPod" +msgstr "Drücke „Auswählen“, um deinen iPod zu wählen" + +#: GUI/widgets/sidebar.py:918 +msgid "Preview and apply iPod-friendly tag fixes across the whole library." +msgstr "Preview and apply iPod-friendly tag fixes across the whole library." + +#: GUI/widgets/settingsPage.py:1658 +msgid "Rating Conflict Strategy" +msgstr "Strategie bei Bewertungskonflikten" + +msgid "REGULAR PLAYLISTS" +msgstr "NORMALE WIEDERGABELISTEN" + +#: GUI/app.py:488 +msgid "Reading library and device settings." +msgstr "Mediathek und Geräteeinstellungen werden gelesen." + +#: GUI/widgets/settingsPage.py:1741 +msgid "Reencode Existing Lossy Files" +msgstr "Vorhandene verlustbehaftete Dateien neu codieren" + +#: GUI/widgets/settingsPage.py:1561 +msgid "Report a Bug" +msgstr "Fehler melden" + +#: GUI/widgets/sidebar.py:873 +msgid "Rescan" +msgstr "Neu scannen" + +#: GUI/widgets/settingsPage.py:1481 +msgid "Reset" +msgstr "Zurücksetzen" + +#: GUI/widgets/settingsPage.py:1481 +msgid "Reset Device Settings" +msgstr "Geräteeinstellungen zurücksetzen" + +#: GUI/widgets/settingsPage.py:553 +msgid "Reset to auto-detect" +msgstr "Auf automatische Erkennung zurücksetzen" + +msgid "Size" +msgstr "Größe" + +msgid "Sort" +msgstr "Sortieren" + +msgid "SMART PLAYLISTS" +msgstr "INTELLIGENTE WIEDERGABELISTEN" + +msgid "INTERNAL BROWSE CATEGORIES" +msgstr "INTERNE SUCHKATEGORIEN" + +#: GUI/widgets/settingsPage.py:419 +msgid "Reset to default" +msgstr "Auf Standard zurücksetzen" + +#: GUI/widgets/settingsPage.py:1647 +msgid "Rotate Tall Photos on Device" +msgstr "Hochformatfotos auf dem Gerät drehen" + +#: GUI/widgets/settingsPage.py:1539 +msgid "Round album art corners in grid cards and track lists. This only changes how artwork is drawn in iOpenPod and does not modify anything written to your iPod." +msgstr "Round album art corners in grid cards and track lists. This only changes how artwork is drawn in iOpenPod and does not modify anything written to your iPod." + +#: GUI/widgets/settingsPage.py:1539 +msgid "Rounded Artwork" +msgstr "Abgerundetes Artwork" + +#: GUI/widgets/sidebar.py:217 +msgid "Safely eject the iPod from your system" +msgstr "iPod sicher aus dem System auswerfen" + +#: GUI/widgets/sidebar.py:774 +msgid "Save failed" +msgstr "Speichern fehlgeschlagen" + +#: GUI/widgets/sidebar.py:767 +msgid "Saved" +msgstr "Gespeichert" + +#: GUI/widgets/sidebar.py:761 +msgid "Saving…" +msgstr "Wird gespeichert…" + +#: GUI/widgets/settingsPage.py:1515 +msgid "Scale text size across the interface for accessibility." +msgstr "Scale text size across the interface for accessibility." + +#: GUI/widgets/sidebar.py:611 +msgid "Scheme 1" +msgstr "Schema 1" + +#: GUI/widgets/sidebar.py:611 +msgid "Scheme 2" +msgstr "Schema 2" + +#: GUI/widgets/settingsPage.py:1928 +msgid "Scrobbling" +msgstr "Scrobbling" + +#: GUI/widgets/sidebar.py:872 +msgid "Select" +msgstr "Auswählen" + +#: GUI/app.py:451 +msgid "Select Device" +msgstr "Gerät auswählen" + +#: GUI/widgets/settingsPage.py:579 +msgid "Select File" +msgstr "Datei auswählen" + +#: GUI/widgets/settingsPage.py:350 GUI/widgets/settingsPage.py:464 +msgid "Select Folder" +msgstr "Ordner auswählen" + +#: GUI/app.py:434 +msgid "Select an iPod to continue" +msgstr "Wähle einen iPod, um fortzufahren" + +#: GUI/widgets/settingsPage.py:2060 +msgid "Select an iPod to edit device settings" +msgstr "Wähle einen iPod, um Geräteeinstellungen zu bearbeiten" + +#: GUI/widgets/settingsPage.py:1335 GUI/widgets/sidebar.py:977 +msgid "Settings" +msgstr "Einstellungen" + +msgid "Settings Folder" +msgstr "Einstellungsordner" + +#: GUI/widgets/settingsPage.py:1546 +msgid "Sharpen Artwork" +msgstr "Artwork schärfen" + +#: GUI/widgets/settingsPage.py:1534 +msgid "Show album art thumbnails next to tracks in the list view." +msgstr "Show album art thumbnails next to tracks in the list view." + +#: GUI/widgets/settingsPage.py:1775 +msgid "Smart Quality by Content Type" +msgstr "Intelligente Qualität nach Inhaltstyp" + +#: GUI/widgets/sidebar.py:275 +msgid "Songs" +msgstr "Titel" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Spoken Word" +msgstr "Sprache" + +#: GUI/widgets/settingsPage.py:1728 +msgid "Spoken Word Bitrate" +msgstr "Bitrate für Sprache" + +#: GUI/widgets/settingsPage.py:1893 +msgid "Status" +msgstr "Status" + +#: GUI/widgets/settingsPage.py:1976 GUI/widgets/sidebar.py:401 +msgid "Storage" +msgstr "Speicher" + +msgid "Storage Paths" +msgstr "Speicherpfade" + +#: GUI/widgets/settingsPage.py:1572 +msgid "Support iOpenPod" +msgstr "iOpenPod unterstützen" + +#: GUI/widgets/settingsPage.py:1676 +msgid "Sync" +msgstr "Synchronisierung" + +#: GUI/widgets/sidebar.py:896 +msgid "Sync with PC" +msgstr "Mit PC synchronisieren" + +msgid "TV Shows" +msgstr "TV-Sendungen" + +#: GUI/widgets/sidebar.py:284 +msgid "Technical Details" +msgstr "Technische Details" + +#: GUI/widgets/settingsPage.py:1801 +msgid "Temporal Noise Shaping (TNS)" +msgstr "Temporal Noise Shaping (TNS)" + +#: GUI/app.py:612 +msgid "The selected folder could not be identified as an iPod." +msgstr "The selected folder could not be identified as an iPod." + +#: GUI/app.py:628 +msgid "" +"The selected folder does not appear to be a valid iPod root.\n" +"\n" +"Expected structure:\n" +" /iPod_Control/iTunes/\n" +"\n" +"Please select the root folder of your iPod." +msgstr "" +"The selected folder does not appear to be a valid iPod root.\n" +"\n" +"Expected structure:\n" +" /iPod_Control/iTunes/\n" +"\n" +"Please select the root folder of your iPod." + +#: GUI/widgets/settingsPage.py:1495 +msgid "Theme" +msgstr "Design" + +#: GUI/widgets/settingsPage.py:1534 +msgid "Track List Artwork" +msgstr "Artwork in Titelliste" + +#: GUI/widgets/trackListTitleBar.py:141 +msgid "Tracks" +msgstr "Titel" + +#: GUI/widgets/settingsPage.py:1976 +msgid "Transcode Cache" +msgstr "Transcode-Cache" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Transcoding" +msgstr "Transcodierung" + +#: GUI/widgets/settingsPage.py:1475 +msgid "Use Global Settings" +msgstr "Globale Einstellungen verwenden" + +#: GUI/widgets/settingsPage.py:1722 +msgid "VBR Quality Level" +msgstr "VBR-Qualitätsstufe" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Video" +msgstr "Video" + +#: GUI/widgets/settingsPage.py:1762 +msgid "Video Encode Speed" +msgstr "Video-Codiergeschwindigkeit" + +#: GUI/widgets/settingsPage.py:1751 +msgid "Video Quality (CRF)" +msgstr "Videoqualität (CRF)" + +msgid "Videos" +msgstr "Videos" + +#: GUI/widgets/settingsPage.py:1016 +msgid "Waiting for browser approval..." +msgstr "Warte auf Browserbestätigung..." + +#: GUI/widgets/settingsPage.py:1635 +msgid "Write Back to PC" +msgstr "Auf PC zurückschreiben" + +#: GUI/widgets/MBListView.py:168 GUI/widgets/MBListView.py:173 GUI/widgets/podcastBrowser.py:1767 GUI/widgets/sidebar.py:41 +msgid "Yes" +msgstr "Ja" + +#: GUI/widgets/settingsPage.py:1875 +msgid "fpcalc (Chromaprint)" +msgstr "fpcalc (Chromaprint)" + +#: GUI/widgets/settingsPage.py:1887 +msgid "fpcalc Path Override" +msgstr "fpcalc Path Override" + +msgid "iOpenPod Error" +msgstr "iOpenPod-Fehler" + +#: GUI/widgets/settingsPage.py:1572 +msgid "iOpenPod is and always will be completely free and open source. If you like it and would like to support me, it is so very appreciated." +msgstr "iOpenPod is and always will be completely free and open source. If you like it and would like to support me, it is so very appreciated." + +#: GUI/app.py:661 +msgid "iPod Not Writable" +msgstr "iPod nicht beschreibbar" + +#: GUI/widgets/settingsPage.py:1795 +msgid "libfdk_aac Afterburner" +msgstr "libfdk_aac Afterburner" + +#: GUI/widgets/sidebar.py:675 GUI/widgets/sidebar.py:681 +msgid "{free:.1f} GB free of {total:.1f} GB" +msgstr "{free:.1f} GB frei von {total:.1f} GB" + +#: GUI/widgets/MBListView.py:2746 +msgid "(all columns shown)" +msgstr "(alle Spalten sichtbar)" + +msgid "{count:,} audiobook" +msgstr "{count:,} Hörbuch" + +msgid "{count:,} audiobooks" +msgstr "{count:,} Hörbücher" + +msgid "{count:,} episode" +msgstr "{count:,} Folge" + +msgid "{count:,} episodes" +msgstr "{count:,} Folgen" + +msgid "{count:,} song" +msgstr "{count:,} Titel" + +msgid "{count:,} songs" +msgstr "{count:,} Titel" + +msgid "{count:,} track" +msgstr "{count:,} Titel" + +msgid "{count:,} tracks" +msgstr "{count:,} Titel" + +msgid "{count:,} video" +msgstr "{count:,} Video" + +msgid "{count:,} videos" +msgstr "{count:,} Videos" + +msgid "{shown:,} of {total:,} audiobook" +msgstr "{shown:,} of {total:,} audiobook" + +msgid "{shown:,} of {total:,} audiobooks" +msgstr "{shown:,} of {total:,} audiobooks" + +msgid "{shown:,} of {total:,} episode" +msgstr "{shown:,} von {total:,} Folge" + +msgid "{shown:,} of {total:,} episodes" +msgstr "{shown:,} von {total:,} Folgen" + +msgid "{shown:,} of {total:,} song" +msgstr "{shown:,} von {total:,} Titeln" + +msgid "{shown:,} of {total:,} songs" +msgstr "{shown:,} von {total:,} Titeln" + +msgid "{shown:,} of {total:,} track" +msgstr "{shown:,} von {total:,} Titel" + +msgid "{shown:,} of {total:,} tracks" +msgstr "{shown:,} von {total:,} Titeln" + +msgid "{shown:,} of {total:,} video" +msgstr "{shown:,} von {total:,} Video" + +msgid "{shown:,} of {total:,} videos" +msgstr "{shown:,} von {total:,} Videos" + +msgid "{gb:.2f} GB used of {max_gb:.0f} GB · {count:,} file" +msgstr "{gb:.2f} GB von {max_gb:.0f} GB belegt · {count:,} Datei" + +msgid "{gb:.2f} GB used of {max_gb:.0f} GB · {count:,} files" +msgstr "{gb:.2f} GB von {max_gb:.0f} GB belegt · {count:,} Dateien" + +msgid "{gb:.2f} GB · {count:,} file" +msgstr "{gb:.2f} GB · {count:,} Datei" + +msgid "{gb:.2f} GB · {count:,} files" +msgstr "{gb:.2f} GB · {count:,} Dateien" + +#: GUI/widgets/MBListView.py:2646 +msgid "Add Column" +msgstr "Spalte hinzufügen" + +msgid "Album Artist" +msgstr "Albumkünstler" + +msgid "Album ID" +msgstr "Album-ID" + +msgid "Art Count" +msgstr "Artwork-Anzahl" + +msgid "Art Formats:" +msgstr "Artwork-Formate:" + +msgid "Artist Ref" +msgstr "Künstlerreferenz" + +msgid "Artwork" +msgstr "Artwork" + +msgid "Artwork Ref" +msgstr "Artwork-Referenz" + +msgid "Audio:" +msgstr "Audio:" + +msgid "Audio Quality" +msgstr "Audioqualität" + +msgid "Bitrate" +msgstr "Bitrate" + +msgid "BPM" +msgstr "BPM" + +msgid "Bus/Format:" +msgstr "Bus/Format:" + +#: GUI/widgets/settingsPage.py:1943 +msgid "Cache Location" +msgstr "Cache-Speicherort" + +#: GUI/widgets/sidebar.py:392 +msgid "Capabilities" +msgstr "Funktionen" + +msgid "Category" +msgstr "Kategorie" + +msgid "Chapter Img:" +msgstr "Chapter Img:" + +msgid "Chapter Titles" +msgstr "Kapiteltitel" + +msgid "Chapters" +msgstr "Kapitel" + +msgid "Cleared — {count:,} file removed" +msgstr "Geleert — {count:,} Datei entfernt" + +msgid "Cleared — {count:,} files removed" +msgstr "Geleert — {count:,} Dateien entfernt" + +msgid "Comment" +msgstr "Kommentar" + +msgid "Compilation" +msgstr "Compilation" + +msgid "Composer" +msgstr "Komponist" + +msgid "Composer ID" +msgstr "Composer ID" + +msgid "Conflicts:" +msgstr "Konflikte:" + +#: GUI/widgets/musicBrowser.py:471 +msgid "Convert to a single chaptered track" +msgstr "In einen einzelnen Titel mit Kapiteln umwandeln" + +msgid "Core Metadata" +msgstr "Kernmetadaten" + +#: GUI/widgets/musicBrowser.py:560 +msgid "" +"Could not prepare artwork:\n" +"\n" +"{error}" +msgstr "" +"Could not prepare artwork:\n" +"\n" +"{error}" + +#: GUI/widgets/musicBrowser.py:582 +msgid "" +"Could not stage artwork update:\n" +"\n" +"{error}" +msgstr "" +"Could not stage artwork update:\n" +"\n" +"{error}" + +#: GUI/widgets/settingsPage.py:1961 +msgid "Custom directory to store iOpenPod settings. Useful for portable setups or backups." +msgstr "Custom directory to store iOpenPod settings. Useful for portable setups or backups." + +msgid "Date Added" +msgstr "Hinzugefügt am" + +msgid "Date Modified" +msgstr "Geändert am" + +msgid "Dates" +msgstr "Daten" + +msgid "Description" +msgstr "Beschreibung" + +msgid "Device DB:" +msgstr "Device DB:" + +msgid "Disc #" +msgstr "CD-Nr." + +msgid "Disc Total" +msgstr "CDs gesamt" + +msgid "Disk Size:" +msgstr "Datenträgergröße:" + +msgid "Encoder" +msgstr "Encoder" + +msgid "Enclosure URL" +msgstr "Enclosure URL" + +msgid "Episode #" +msgstr "Folge Nr." + +msgid "Episode ID" +msgstr "Folgen-ID" + +msgid "Equalizer" +msgstr "Equalizer" + +#: GUI/widgets/settingsPage.py:1194 +msgid "Error clearing cache: {error}" +msgstr "Error clearing cache: {error}" + +msgid "File Format" +msgstr "Dateiformat" + +msgid "Flags" +msgstr "Flags" + +msgid "Free Space:" +msgstr "Freier Speicher:" + +msgid "Gapless" +msgstr "Gapless" + +msgid "Gapless Album" +msgstr "Gapless Album" + +msgid "Gapless Payload" +msgstr "Gapless Payload" + +msgid "Grouping" +msgstr "Gruppierung" + +msgid "Has Lyrics" +msgstr "Hat Liedtext" + +msgid "Hash Scheme:" +msgstr "Hash Scheme:" + +#: GUI/widgets/MBListView.py:2628 +msgid "Hide \"{column}\"" +msgstr "„{column}“ ausblenden" + +#: GUI/widgets/sidebar.py:364 +msgid "Identity" +msgstr "Identität" + +msgid "Identifiers" +msgstr "Kennungen" + +msgid "Keywords" +msgstr "Schlagwörter" + +msgid "Largest" +msgstr "Größte" + +msgid "Last Played" +msgstr "Zuletzt gespielt" + +msgid "Last Skipped" +msgstr "Zuletzt übersprungen" + +#: GUI/widgets/settingsPage.py:1976 +msgid "Locations" +msgstr "Speicherorte" + +msgid "Locale" +msgstr "Gebietsschema" + +msgid "Location" +msgstr "Ort" + +#: GUI/widgets/settingsPage.py:1968 +msgid "Log Location" +msgstr "Log-Speicherort" + +msgid "Lyrics" +msgstr "Liedtext" + +#: GUI/widgets/settingsPage.py:1950 +msgid "Max Cache Size" +msgstr "Maximale Cache-Größe" + +msgid "Max File:" +msgstr "Max File:" + +msgid "Max Transfer:" +msgstr "Max Transfer:" + +msgid "Media Type" +msgstr "Medientyp" + +#: GUI/widgets/trackListTitleBar.py:146 +msgid "Minimize" +msgstr "Minimieren" + +msgid "Model #:" +msgstr "Model #:" + +msgid "Most Albums" +msgstr "Meiste Alben" + +msgid "Most Artists" +msgstr "Meiste Künstler" + +msgid "Most Plays" +msgstr "Meiste Wiedergaben" + +msgid "Most Skipped" +msgstr "Meist übersprungen" + +msgid "Most Tracks" +msgstr "Meiste Titel" + +msgid "Network" +msgstr "Netzwerk" + +#: GUI/widgets/settingsPage.py:1950 +msgid "Oldest cached files are automatically removed (LRU) to stay within this limit. Set to Unlimited if storage is not a concern." +msgstr "Oldest cached files are automatically removed (LRU) to stay within this limit. Set to Unlimited if storage is not a concern." + +#: GUI/widgets/MBListView.py:2736 +msgid "Other" +msgstr "Andere" + +msgid "Photo Formats:" +msgstr "Fotoformate:" + +msgid "Playback && Stats" +msgstr "Wiedergabe && Statistik" + +msgid "Played" +msgstr "Gespielt" + +msgid "Plays" +msgstr "Wiedergaben" + +msgid "Plays (iPod)" +msgstr "Wiedergaben (iPod)" + +msgid "Podcast" +msgstr "Podcast" + +msgid "Podcasts:" +msgstr "Podcasts:" + +msgid "Post-gap" +msgstr "Nachlücke" + +msgid "Pre-gap" +msgstr "Vorlücke" + +msgid "Product:" +msgstr "Product:" + +#: GUI/widgets/MBListView.py:3312 +msgid "Rating" +msgstr "Bewertung" + +msgid "Release Date" +msgstr "Veröffentlichungsdatum" + +msgid "Remember Pos." +msgstr "Position merken" + +#: GUI/widgets/MBListView.py:2640 +msgid "Resize All Columns to Fit" +msgstr "Alle Spalten passend anpassen" + +#: GUI/widgets/MBListView.py:2635 +msgid "Resize Column to Fit" +msgstr "Spalte passend anpassen" + +msgid "RSS URL" +msgstr "RSS-URL" + +msgid "Sample Count" +msgstr "Sample-Anzahl" + +msgid "Sample Rate" +msgstr "Abtastrate" + +#: GUI/widgets/gridHeaderBar.py:77 +msgid "Search…" +msgstr "Suchen…" + +msgid "Season" +msgstr "Staffel" + +msgid "Select an Album" +msgstr "Album auswählen" + +msgid "Select an Artist" +msgstr "Künstler auswählen" + +msgid "Select a Genre" +msgstr "Genre auswählen" + +msgid "Serial:" +msgstr "Serial:" + +#: GUI/widgets/settingsPage.py:1961 +msgid "Settings Location" +msgstr "Einstellungen-Speicherort" + +msgid "Shadow DB:" +msgstr "Shadow DB:" + +msgid "Show" +msgstr "Sendung" + +msgid "Skip Shuffle" +msgstr "Bei Zufall überspringen" + +msgid "Skips" +msgstr "Übersprungen" + +#: GUI/widgets/gridHeaderBar.py:153 +msgid "Sort: {label} ▾" +msgstr "Sortieren: {label} ▾" + +msgid "Sort Album" +msgstr "Album sortieren" + +msgid "Sort Album Artist" +msgstr "Albumkünstler sortieren" + +msgid "Sort Artist" +msgstr "Künstler sortieren" + +msgid "Sort Composer" +msgstr "Komponist sortieren" + +msgid "Sort Overrides" +msgstr "Sortierüberschreibungen" + +msgid "Sort Show" +msgstr "Sendung sortieren" + +msgid "Sort Title" +msgstr "Titel sortieren" + +msgid "Sparse Art:" +msgstr "Sparse Art:" + +msgid "Start Time" +msgstr "Startzeit" + +msgid "Stop Time" +msgstr "Stoppzeit" + +msgid "Subtitle" +msgstr "Untertitel" + +msgid "Time" +msgstr "Zeit" + +msgid "Title" +msgstr "Titel" + +msgid "Track #" +msgstr "Titel Nr." + +msgid "Track ID" +msgstr "Titel-ID" + +msgid "Track Keywords" +msgstr "Titel-Schlagwörter" + +msgid "Track Total" +msgstr "Titel gesamt" + +#: GUI/widgets/settingsPage.py:1167 +msgid "Unavailable ({error})" +msgstr "Nicht verfügbar ({error})" + +#: GUI/widgets/musicBrowser.py:488 GUI/widgets/musicBrowser.py:559 GUI/widgets/musicBrowser.py:581 +msgid "Unify Artwork" +msgstr "Artwork vereinheitlichen" + +msgid "Updater ID:" +msgstr "Updater ID:" + +#: GUI/widgets/sidebar.py:374 +msgid "USB / SCSI" +msgstr "USB / SCSI" + +msgid "USB Parent:" +msgstr "USB Parent:" + +msgid "USB PID:" +msgstr "USB PID:" + +msgid "USB Serial:" +msgstr "USB Serial:" + +msgid "USB VID:" +msgstr "USB VID:" + +msgid "Voice Memos:" +msgstr "Voice Memos:" + +msgid "Volume Adj." +msgstr "Lautstärkeanpassung" + +#: GUI/widgets/settingsPage.py:1968 +msgid "Where iOpenPod writes log files and crash reports. Takes effect on next launch." +msgstr "Hier schreibt iOpenPod Protokolle und Absturzberichte. Wird beim nächsten Start wirksam." + +#: GUI/widgets/settingsPage.py:1943 +msgid "Where transcoded files are cached to avoid re-encoding on future syncs." +msgstr "Hier werden transcodierte Dateien zwischengespeichert, um erneute Codierung bei späteren Synchronisierungen zu vermeiden." + +msgid "Year" +msgstr "Jahr" + +#: GUI/widgets/sidebar.py:546 +msgid "" +"{value}\n" +"Source: {source}" +msgstr "" +"{value}\n" +"Source: {source}" + +#: GUI/widgets/MBListView.py:3322 GUI/widgets/MBListView.py:3360 +msgid "(mixed selection)" +msgstr "(gemischte Auswahl)" + +#: GUI/widgets/MBListView.py:3102 +msgid "Add to Playlist" +msgstr "Zur Wiedergabeliste hinzufügen" + +#: GUI/widgets/MBListView.py:3304 +msgid "Checked" +msgstr "Markiert" + +#: GUI/widgets/MBListView.py:3351 +msgid "Content Advisory" +msgstr "Inhaltshinweis" + +#: GUI/widgets/MBListView.py:2364 +msgid "Content Advisory: Clean" +msgstr "Inhaltshinweis: sauber" + +#: GUI/widgets/MBListView.py:2356 +msgid "Content Advisory: Explicit" +msgstr "Inhaltshinweis: explizit" + +#: GUI/widgets/MBListView.py:3209 +msgid "Convert to Podcast" +msgstr "In Podcast umwandeln" + +#: GUI/widgets/MBListView.py:3191 +msgid "Copy as File(s)" +msgstr "Als Datei(en) kopieren" + +#: GUI/widgets/MBListView.py:3188 +msgid "Copy as Text" +msgstr "Als Text kopieren" + +#: GUI/widgets/settingsPage.py:2959 +msgid "Downloading update…" +msgstr "Update wird heruntergeladen…" + +#: GUI/widgets/MBListView.py:2908 +msgid "Edit ({count})" +msgstr "Bearbeiten ({count})" + +#: GUI/widgets/MBListView.py:153 GUI/widgets/MBListView.py:3367 +msgid "Explicit" +msgstr "Explizit" + +#: GUI/widgets/settingsPage.py:3060 +msgid "ffprobe missing" +msgstr "ffprobe missing" + +#: GUI/widgets/settingsPage.py:2961 +msgid "iOpenPod Update" +msgstr "iOpenPod Update" + +#: GUI/widgets/settingsPage.py:3214 +msgid "Invalid token" +msgstr "Ungültiger Token" + +#: GUI/widgets/MBListView.py:3166 +msgid "Move Down" +msgstr "Nach unten" + +#: GUI/widgets/MBListView.py:3162 +msgid "Move Up" +msgstr "Nach oben" + +#: GUI/widgets/MBListView.py:3106 GUI/widgets/playlistBrowser.py:1331 GUI/widgets/playlistBrowser.py:1674 +msgid "New Playlist" +msgstr "Neue Wiedergabeliste" + +#: GUI/widgets/settingsPage.py:2948 +msgid "No Binary Available" +msgstr "No Binary Available" + +#: GUI/widgets/settingsPage.py:2949 +msgid "" +"No pre-built binary was found for your platform.\n" +"\n" +"Visit {release_page} to download manually." +msgstr "" +"No pre-built binary was found for your platform.\n" +"\n" +"Visit {release_page} to download manually." + +#: GUI/widgets/MBListView.py:3328 +msgid "No Rating" +msgstr "Keine Bewertung" + +#: GUI/widgets/MBListView.py:3366 +msgid "None (Unset)" +msgstr "Keine (nicht gesetzt)" + +msgid "Part of a compilation album" +msgstr "Teil eines Compilation-Albums" + +msgid "Preparing {count} file…" +msgstr "{count} Datei wird vorbereitet…" + +msgid "Preparing {count} files…" +msgstr "{count} Dateien werden vorbereitet…" + +msgid "Remove {count} Track from iPod" +msgstr "{count} Titel vom iPod entfernen" + +msgid "Remove {count} Tracks from iPod" +msgstr "{count} Titel vom iPod entfernen" + +msgid "Remove {count} Track from Playlist" +msgstr "{count} Titel aus Wiedergabeliste entfernen" + +msgid "Remove {count} Tracks from Playlist" +msgstr "{count} Titel aus Wiedergabeliste entfernen" + +#: GUI/widgets/MBListView.py:2752 +msgid "Reset Columns" +msgstr "Spalten zurücksetzen" + +msgid "Skip When Shuffling" +msgstr "Bei Zufallswiedergabe überspringen" + +msgid "Skip this track in shuffle mode" +msgstr "Diesen Titel im Zufallsmodus überspringen" + +#: GUI/widgets/MBListView.py:3074 +msgid "Split chapters into individual tracks" +msgstr "Kapitel in einzelne Titel aufteilen" + +#: GUI/widgets/MBListView.py:773 +msgid "Starting drag…" +msgstr "Ziehen wird gestartet…" + +#: GUI/widgets/MBListView.py:3116 +msgid "Untitled" +msgstr "Ohne Titel" + +#: GUI/widgets/settingsPage.py:2892 +msgid "Up to Date" +msgstr "Aktuell" + +#: GUI/widgets/settingsPage.py:2886 +msgid "Update Check Failed" +msgstr "Updateprüfung fehlgeschlagen" + +#: GUI/widgets/settingsPage.py:3180 +msgid "Validating…" +msgstr "Wird validiert…" + +#: GUI/widgets/MBListView.py:3384 +msgid "Volume Adjustment" +msgstr "Lautstärkeanpassung" + +#: GUI/widgets/settingsPage.py:2893 +msgid "You are running the latest version (v{version})." +msgstr "You are running the latest version (v{version})." + +#: GUI/widgets/MBListView.py:155 GUI/widgets/MBListView.py:3368 +msgid "Clean" +msgstr "Sauber" + +#: GUI/widgets/sidebar.py:383 +msgid "Database" +msgstr "Datenbank" + +#: GUI/widgets/musicBrowser.py:463 GUI/widgets/playlistBrowser.py:419 +msgid "Edit" +msgstr "Bearbeiten" + +#: GUI/widgets/trackListTitleBar.py:150 +msgid "Maximize" +msgstr "Maximieren" + +#: GUI/widgets/MBListView.py:215 +msgid "{count} chapter" +msgstr "{count} Kapitel" + +#: GUI/widgets/MBListView.py:216 +msgid "{count} chapters" +msgstr "{count} Kapitel" + +#: GUI/app.py:2312 +msgid "" +"\n" +"\n" +"If you discard, the copied files will be cleaned up automatically the next time you sync." +msgstr "" +"\n" +"\n" +"If you discard, the copied files will be cleaned up automatically the next time you sync." + +#: app_core/jobs.py:70 +msgid " and " +msgstr " and " + +#: GUI/app.py:1439 GUI/app.py:1433 +msgid "Album Conversion" +msgstr "Albumkonvertierung" + +#: GUI/app.py:1594 +msgid "Chapter Split" +msgstr "Kapitelaufteilung" + +#: GUI/app.py:1440 +msgid "Choose an album with at least two tracks." +msgstr "Wähle ein Album mit mindestens zwei Titeln." + +#: GUI/app.py:781 +msgid "Could Not Load iPod" +msgstr "iPod konnte nicht geladen werden" + +#: GUI/app.py:1379 +msgid "" +"Could not download sync tools:\n" +"\n" +"{error}" +msgstr "" +"Could not download sync tools:\n" +"\n" +"{error}" + +#: GUI/app.py:1102 +msgid "" +"Could not save quick changes to iPod:\n" +"{error}\n" +"\n" +"iOpenPod is reloading the device view from the iPod." +msgstr "" +"Could not save quick changes to iPod:\n" +"{error}\n" +"\n" +"iOpenPod is reloading the device view from the iPod." + +#: GUI/app.py:955 app_core/jobs.py:1940 app_core/jobs.py:1989 app_core/jobs.py:2265 +msgid "Database write failed." +msgstr "Schreiben der Datenbank fehlgeschlagen." + +#: GUI/app.py:958 +msgid "Device name updated successfully" +msgstr "Gerätename erfolgreich aktualisiert" + +#: GUI/app.py:2317 +msgid "Discard" +msgstr "Verwerfen" + +#: GUI/app.py:1378 +msgid "Download Failed" +msgstr "Download fehlgeschlagen" + +#: GUI/app.py:2625 GUI/widgets/podcastBrowser.py:237 +msgid "Downloading" +msgstr "Wird heruntergeladen" + +#: GUI/app.py:2638 +msgid "Downloading Tools…" +msgstr "Werkzeuge werden heruntergeladen…" + +#: GUI/app.py:1075 +msgid "Eject Failed" +msgstr "Auswerfen fehlgeschlagen" + +#: app_core/jobs.py:84 +msgid "" +"FFmpeg and ffprobe are required for transcoding and media probing.\n" +"Install from: https://ffmpeg.org" +msgstr "" +"FFmpeg and ffprobe are required for transcoding and media probing.\n" +"Install from: https://ffmpeg.org" + +#: GUI/app.py:1076 +msgid "" +"Failed to eject the iPod:\n" +"{error}" +msgstr "" +"Failed to eject the iPod:\n" +"{error}" + +#: GUI/app.py:966 +msgid "" +"Failed to rename iPod:\n" +"{error}" +msgstr "" +"Failed to rename iPod:\n" +"{error}" + +#: GUI/app.py:1423 GUI/app.py:1582 GUI/app.py:1184 +msgid "Library Loading" +msgstr "Mediathek wird geladen" + +#: GUI/app.py:791 +msgid "Load an iPod library before running tag normalization." +msgstr "Load an iPod library before running tag normalization." + +#: GUI/app.py:2525 +msgid "Missing Tools" +msgstr "Fehlende Werkzeuge" + +#: app_core/jobs.py:1922 app_core/jobs.py:1971 +msgid "No iPod connected." +msgstr "Kein iPod verbunden." + +#: app_core/jobs.py:1926 app_core/jobs.py:1975 +msgid "No iPod database loaded." +msgstr "Keine iPod-Datenbank geladen." + +#: GUI/app.py:2087 +msgid "No iPod device selected." +msgstr "Kein iPod-Gerät ausgewählt." + +#: GUI/app.py:824 +msgid "No iPod-specific metadata fixes were found for this library." +msgstr "Für diese Mediathek wurden keine iPod-spezifischen Metadatenkorrekturen gefunden." + +#: GUI/app.py:800 +msgid "No tracks were found in this iPod library." +msgstr "In dieser iPod-Mediathek wurden keine Titel gefunden." + +#: GUI/app.py:790 GUI/app.py:799 GUI/app.py:823 +msgid "Normalize iPod Tags" +msgstr "iPod-Tags normalisieren" + +#: GUI/app.py:2184 +msgid "Not Enough Space" +msgstr "Nicht genug Speicherplatz" + +#: GUI/app.py:2575 +msgid "Not Now" +msgstr "Nicht jetzt" + +#: GUI/app.py:2603 +msgid "OK" +msgstr "OK" + +#: GUI/app.py:1418 GUI/app.py:1575 +msgid "Please select an iPod device first." +msgstr "Bitte zuerst ein iPod-Gerät auswählen." + +#: GUI/app.py:1412 +msgid "Please wait for the current sync to finish before converting an album." +msgstr "Bitte warte, bis die aktuelle Synchronisierung abgeschlossen ist, bevor du ein Album konvertierst." + +#: GUI/app.py:1030 +msgid "Please wait for the current sync to finish before ejecting." +msgstr "Bitte warte, bis die aktuelle Synchronisierung abgeschlossen ist, bevor du auswirfst." + +#: GUI/app.py:1569 +msgid "Please wait for the current sync to finish before splitting chapters." +msgstr "Bitte warte, bis die aktuelle Synchronisierung abgeschlossen ist, bevor du Kapitel aufteilst." + +#: GUI/app.py:1423 GUI/app.py:1583 GUI/app.py:1185 +msgid "Please wait for the iPod library to finish loading." +msgstr "Bitte warte, bis die iPod-Mediathek fertig geladen ist." + +#: GUI/app.py:2644 +msgid "Preparing download…" +msgstr "Download wird vorbereitet…" + +#: GUI/app.py:1174 +msgid "Quick Changes Still Saving" +msgstr "Schnelländerungen werden noch gespeichert" + +#: GUI/app.py:2421 +msgid "Reading dropped files..." +msgstr "Abgelegte Dateien werden gelesen..." + +#: GUI/app.py:965 +msgid "Rename Failed" +msgstr "Umbenennen fehlgeschlagen" + +#: GUI/app.py:1101 +msgid "Save Failed" +msgstr "Speichern fehlgeschlagen" + +#: GUI/app.py:979 +msgid "Save In Progress" +msgstr "Speichern läuft" + +#: GUI/app.py:2316 +msgid "Save Partial Database" +msgstr "Teilweise Datenbank speichern" + +#: GUI/app.py:2301 +msgid "Save Partial Sync?" +msgstr "Teilweise Synchronisierung speichern?" + +#: GUI/app.py:1008 +msgid "Still Reading iPod" +msgstr "iPod wird noch gelesen" + +#: GUI/app.py:2280 +msgid "Sync Error" +msgstr "Synchronisierungsfehler" + +#: GUI/app.py:2204 +msgid "Sync Failed" +msgstr "Synchronisierung fehlgeschlagen" + +#: GUI/app.py:1029 +msgid "Sync In Progress" +msgstr "Synchronisierung läuft" + +#: GUI/app.py:1411 GUI/app.py:1568 +msgid "Sync Running" +msgstr "Synchronisierung aktiv" + +#: GUI/app.py:2189 +msgid "Sync Until Full" +msgstr "Synchronisieren bis voll" + +#: GUI/app.py:167 +msgid "Sync failed before making changes." +msgstr "Die Synchronisierung ist vor Änderungen fehlgeschlagen." + +#: GUI/app.py:2185 +msgid "The selected sync is larger than the iPod's free space." +msgstr "Die ausgewählte Synchronisierung ist größer als der freie Speicher des iPod." + +#: GUI/app.py:1393 +msgid "This iPod does not support podcasts." +msgstr "Dieser iPod unterstützt keine Podcasts." + +#: GUI/app.py:2166 +msgid "" +"This sync is estimated to need more space than is available on the iPod.\n" +"\n" +"Available: {available}\n" +"Estimated needed: {required}\n" +"Estimated shortfall: {shortage}\n" +"\n" +"Sync Until Full will copy files in order until the next file would leave less than {reserve} free, then save the database with the items that actually synced." +msgstr "" +"This sync is estimated to need more space than is available on the iPod.\n" +"\n" +"Available: {available}\n" +"Estimated needed: {required}\n" +"Estimated shortfall: {shortage}\n" +"\n" +"Sync Until Full will copy files in order until the next file would leave less than {reserve} free, then save the database with the items that actually synced." + +#: GUI/app.py:746 GUI/app.py:748 +msgid "Unknown iPod" +msgstr "Unbekannter iPod" + +#: GUI/app.py:1392 +msgid "Unsupported iPod" +msgstr "Nicht unterstützter iPod" + +#: GUI/app.py:2309 +msgid "Would you like to save these tracks to your iPod's database?" +msgstr "Möchtest du diese Titel in der iPod-Datenbank speichern?" + +#: app_core/jobs.py:89 +msgid "" +"You can also set custom paths in\n" +"Settings -> External Tools." +msgstr "" +"Du kannst eigene Pfade auch unter\n" +"Einstellungen -> Externe Werkzeuge festlegen." + +#: app_core/jobs.py:77 +msgid "" +"fpcalc is required for sync.\n" +"Install from: https://acoustid.org/chromaprint" +msgstr "" +"fpcalc is required for sync.\n" +"Install from: https://acoustid.org/chromaprint" + +#: GUI/app.py:2555 +msgid "" +"iOpenPod can download these automatically (~80 MB).\n" +"Download now?" +msgstr "" +"iOpenPod can download these automatically (~80 MB).\n" +"Download now?" + +#: GUI/app.py:186 +msgid "" +"iOpenPod could not load this iPod library.\n" +"\n" +"{error}" +msgstr "" +"iOpenPod could not load this iPod library.\n" +"\n" +"{error}" + +#: GUI/app.py:190 +msgid "" +"iOpenPod could not read this iPod cleanly.\n" +"\n" +"Mount path: {mount}\n" +"System error: {error}\n" +"\n" +"On Linux, this usually means the iPod mount is not accessible, the FAT filesystem is dirty, or the current user does not have permission to the mount.\n" +"\n" +"Try reconnecting the iPod. If it still fails, try remounting it read-write:\n" +" sudo mount -o remount,rw {quoted_mount}\n" +"\n" +"If the filesystem is dirty, unmount it before repairing it:\n" +" sudo umount {quoted_mount}\n" +" sudo fsck.vfat -a /dev/sdXN\n" +"\n" +"Replace /dev/sdXN with the iPod partition. Do not run fsck while the iPod is mounted." +msgstr "" +"iOpenPod could not read this iPod cleanly.\n" +"\n" +"Mount path: {mount}\n" +"System error: {error}\n" +"\n" +"On Linux, this usually means the iPod mount is not accessible, the FAT filesystem is dirty, or the current user does not have permission to the mount.\n" +"\n" +"Try reconnecting the iPod. If it still fails, try remounting it read-write:\n" +" sudo mount -o remount,rw {quoted_mount}\n" +"\n" +"If the filesystem is dirty, unmount it before repairing it:\n" +" sudo umount {quoted_mount}\n" +" sudo fsck.vfat -a /dev/sdXN\n" +"\n" +"Replace /dev/sdXN with the iPod partition. Do not run fsck while the iPod is mounted." + +#: GUI/app.py:1009 +msgid "iOpenPod is still finishing background reads from the iPod. Try ejecting again in a moment." +msgstr "iOpenPod is still finishing background reads from the iPod. Try ejecting again in a moment." + +#: GUI/app.py:1175 +msgid "iOpenPod is still saving pending quick changes. Please wait for {label} to finish before starting a full sync." +msgstr "iOpenPod is still saving pending quick changes. Please wait for {label} to finish before starting a full sync." + +#: GUI/app.py:980 +msgid "iOpenPod is still saving {label} to the iPod. Try ejecting again when the save finishes." +msgstr "iOpenPod is still saving {label} to the iPod. Try ejecting again when the save finishes." + +#: GUI/app.py:1050 +msgid "iPod Ejected" +msgstr "iPod ausgeworfen" + +#: GUI/app.py:958 +msgid "iPod Renamed" +msgstr "iPod umbenannt" + +#: GUI/app.py:1171 +msgid "quick changes" +msgstr "Schnelländerungen" + +#: GUI/app.py:2290 +msgid "track" +msgstr "Titel" + +#: GUI/app.py:2290 GUI/widgets/playlistBrowser.py:1251 +msgid "tracks" +msgstr "Titel" + +#: GUI/app.py:2293 +msgid "{count} more track was not copied." +msgstr "{count} weiterer Titel wurde nicht kopiert." + +#: GUI/app.py:2295 +msgid "{count} more tracks were not copied." +msgstr "{count} weitere Titel wurden nicht kopiert." + +#: GUI/app.py:2304 +msgid "{count} {tracks_word} were successfully copied to your iPod before the sync was cancelled." +msgstr "{count} {tracks_word} wurden vor dem Abbruch erfolgreich auf deinen iPod kopiert." + +#: GUI/app.py:2544 +msgid "{tools} Not Found" +msgstr "{tools} nicht gefunden" + +#: GUI/widgets/photoBrowser.py:1694 +msgid "Add Photo to Album" +msgstr "Foto zum Album hinzufügen" + +#: GUI/widgets/photoBrowser.py:1246 GUI/widgets/photoBrowser.py:480 +msgid "Add to Album" +msgstr "Zum Album hinzufügen" + +#: GUI/widgets/photoBrowser.py:1676 +msgid "Album name:" +msgstr "Albumname:" + +#: GUI/widgets/photoBrowser.py:1695 +msgid "Album:" +msgstr "Album:" + +#: GUI/widgets/photoBrowser.py:814 +msgid "All Photos" +msgstr "Alle Fotos" + +#: GUI/widgets/playlistBrowser.py:1012 +msgid "Choose a playlist to inspect its tracks and database metadata" +msgstr "Wähle eine Wiedergabeliste, um Titel und Datenbankmetadaten anzusehen" + +#: GUI/widgets/photoBrowser.py:1689 +msgid "Create another album first, or choose a photo that is not already in every album." +msgstr "Create another album first, or choose a photo that is not already in every album." + +#: GUI/widgets/playlistBrowser.py:437 +msgid "Delete" +msgstr "Löschen" + +#: GUI/widgets/photoBrowser.py:1724 GUI/widgets/photoBrowser.py:1742 +msgid "Delete '{name}' from the iPod now?" +msgstr "Delete '{name}' from the iPod now?" + +#: GUI/widgets/photoBrowser.py:1323 GUI/widgets/photoBrowser.py:1723 +msgid "Delete Album" +msgstr "Album löschen" + +#: GUI/widgets/photoBrowser.py:1267 GUI/widgets/photoBrowser.py:1741 GUI/widgets/photoBrowser.py:482 +msgid "Delete Photo" +msgstr "Foto löschen" + +#: GUI/widgets/playlistBrowser.py:561 +msgid "Details" +msgstr "Details" + +#: GUI/widgets/playlistBrowser.py:451 GUI/widgets/playlistBrowser.py:1854 GUI/widgets/playlistBrowser.py:1879 +msgid "Evaluate Now" +msgstr "Jetzt auswerten" + +#: GUI/widgets/photoBrowser.py:479 GUI/widgets/playlistBrowser.py:473 +msgid "Export" +msgstr "Exportieren" + +#: GUI/widgets/photoBrowser.py:1660 +msgid "Export Album" +msgstr "Album exportieren" + +#: GUI/widgets/photoBrowser.py:1296 +msgid "Export Album..." +msgstr "Album exportieren..." + +#: GUI/widgets/photoBrowser.py:1660 +msgid "Export All Photos" +msgstr "Alle Fotos exportieren" + +#: GUI/widgets/photoBrowser.py:1296 +msgid "Export All Photos..." +msgstr "Alle Fotos exportieren..." + +#: GUI/widgets/photoBrowser.py:1634 +msgid "Export Photo" +msgstr "Foto exportieren" + +#: GUI/widgets/photoBrowser.py:1238 +msgid "Export Photo..." +msgstr "Foto exportieren..." + +#: GUI/widgets/playlistBrowser.py:1338 GUI/widgets/playlistBrowser.py:1974 GUI/widgets/playlistBrowser.py:1603 +msgid "Import Playlist" +msgstr "Wiedergabeliste importieren" + +#: GUI/widgets/playlistBrowser.py:1401 +msgid "Importing Playlist…" +msgstr "Wiedergabeliste wird importiert…" + +#: GUI/widgets/playlistBrowser.py:1603 +msgid "Importing…" +msgstr "Import wird ausgeführt…" + +#: GUI/widgets/photoBrowser.py:423 GUI/widgets/photoBrowser.py:1305 GUI/widgets/photoBrowser.py:1676 +msgid "New Album" +msgstr "Neues Album" + +#: GUI/widgets/photoBrowser.py:1710 +msgid "New album name:" +msgstr "Neuer Albumname:" + +#: GUI/widgets/photoBrowser.py:1688 +msgid "No Available Albums" +msgstr "Keine verfügbaren Alben" + +#: GUI/widgets/photoBrowser.py:1574 GUI/widgets/photoBrowser.py:1650 +msgid "No Photos" +msgstr "Keine Fotos" + +#: GUI/widgets/photoBrowser.py:1454 GUI/widgets/photoBrowser.py:1581 GUI/widgets/photoBrowser.py:1625 GUI/widgets/photoBrowser.py:1655 +msgid "No iPod Connected" +msgstr "Kein iPod verbunden" + +#: GUI/widgets/photoBrowser.py:474 +msgid "No photo selected" +msgstr "Kein Foto ausgewählt" + +#: GUI/widgets/playlistBrowser.py:1177 +msgid "No playlists on this iPod" +msgstr "Keine Wiedergabelisten auf diesem iPod" + +#: GUI/widgets/playlistBrowser.py:1986 +msgid "Parsing playlist…" +msgstr "Wiedergabeliste wird gelesen…" + +#: GUI/widgets/photoBrowser.py:481 +msgid "Remove from Album" +msgstr "Aus Album entfernen" + +#: GUI/widgets/photoBrowser.py:1256 +msgid "Remove from Current Album" +msgstr "Aus aktuellem Album entfernen" + +#: GUI/widgets/photoBrowser.py:1710 GUI/widgets/photoBrowser.py:1316 +msgid "Rename Album" +msgstr "Album umbenennen" + +#: GUI/widgets/playlistBrowser.py:528 +msgid "Rules" +msgstr "Regeln" + +#: GUI/widgets/photoBrowser.py:475 +msgid "Select a photo to inspect its preview and album details." +msgstr "Wähle ein Foto, um Vorschau und Albumdetails zu sehen." + +#: GUI/widgets/playlistBrowser.py:1513 GUI/widgets/playlistBrowser.py:1547 GUI/widgets/playlistBrowser.py:1811 GUI/widgets/playlistBrowser.py:384 GUI/widgets/playlistBrowser.py:1007 +msgid "Select a playlist" +msgstr "Wiedergabeliste auswählen" + +#: GUI/widgets/photoBrowser.py:1455 +msgid "Select an iPod before editing device photos." +msgstr "Wähle einen iPod, bevor du Gerätefotos bearbeitest." + +#: GUI/widgets/photoBrowser.py:1582 GUI/widgets/photoBrowser.py:1626 GUI/widgets/photoBrowser.py:1656 +msgid "Select an iPod before exporting device photos." +msgstr "Wähle einen iPod, bevor du Gerätefotos exportierst." + +#: GUI/widgets/photoBrowser.py:1574 GUI/widgets/photoBrowser.py:1650 +msgid "There are no photos to export." +msgstr "Es gibt keine Fotos zum Exportieren." + +#: GUI/widgets/playlistBrowser.py:1838 +msgid "Writing…" +msgstr "Wird geschrieben…" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "1" +msgstr "1" + +#: GUI/widgets/settingsPage.py:1950 +msgid "1 GB" +msgstr "1 GB" + +#: GUI/widgets/settingsPage.py:2008 GUI/widgets/settingsPage.py:2008 +msgid "10" +msgstr "10" + +#: GUI/widgets/settingsPage.py:1950 +msgid "10 GB" +msgstr "10 GB" + +#: GUI/widgets/settingsPage.py:1515 GUI/widgets/settingsPage.py:1515 +msgid "100%" +msgstr "100%" + +#: GUI/widgets/settingsPage.py:1515 +msgid "110%" +msgstr "110%" + +#: GUI/widgets/settingsPage.py:1515 +msgid "125%" +msgstr "125%" + +#: GUI/widgets/settingsPage.py:1716 +msgid "128 kbps" +msgstr "128 kbps" + +#: GUI/widgets/settingsPage.py:1787 +msgid "15 kHz" +msgstr "15 kHz" + +#: GUI/widgets/settingsPage.py:1515 +msgid "150%" +msgstr "150%" + +#: GUI/widgets/settingsPage.py:1787 +msgid "16 kHz" +msgstr "16 kHz" + +#: GUI/widgets/settingsPage.py:1716 +msgid "160 kbps" +msgstr "160 kbps" + +#: GUI/widgets/settingsPage.py:1787 +msgid "17 kHz" +msgstr "17 kHz" + +#: GUI/widgets/settingsPage.py:1751 +msgid "18 (High)" +msgstr "18 (High)" + +#: GUI/widgets/settingsPage.py:1787 +msgid "18 kHz" +msgstr "18 kHz" + +#: GUI/widgets/settingsPage.py:1787 +msgid "19 kHz" +msgstr "19 kHz" + +#: GUI/widgets/settingsPage.py:1716 GUI/widgets/settingsPage.py:1716 +msgid "192 kbps" +msgstr "192 kbps" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "2" +msgstr "2" + +#: GUI/widgets/settingsPage.py:1950 +msgid "2 GB" +msgstr "2 GB" + +#: GUI/widgets/settingsPage.py:2008 +msgid "20" +msgstr "20" + +#: GUI/widgets/settingsPage.py:1751 +msgid "20 (Good)" +msgstr "20 (Good)" + +#: GUI/widgets/settingsPage.py:1950 +msgid "20 GB" +msgstr "20 GB" + +#: GUI/widgets/settingsPage.py:1787 +msgid "20 kHz" +msgstr "20 kHz" + +#: GUI/widgets/settingsPage.py:1716 +msgid "224 kbps" +msgstr "224 kbps" + +#: GUI/widgets/settingsPage.py:1751 GUI/widgets/settingsPage.py:1751 +msgid "23 (Balanced)" +msgstr "23 (Balanced)" + +#: GUI/widgets/settingsPage.py:1716 +msgid "256 kbps" +msgstr "256 kbps" + +#: GUI/widgets/settingsPage.py:1751 +msgid "26 (Low)" +msgstr "26 (Low)" + +#: GUI/widgets/settingsPage.py:1751 +msgid "28 (Very Low)" +msgstr "28 (Very Low)" + +#: GUI/widgets/settingsPage.py:1728 +msgid "32 kbps" +msgstr "32 kbps" + +#: GUI/widgets/settingsPage.py:1716 +msgid "320 kbps" +msgstr "320 kbps" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "4" +msgstr "4" + +#: GUI/widgets/settingsPage.py:1728 +msgid "48 kbps" +msgstr "48 kbps" + +#: GUI/widgets/settingsPage.py:2008 +msgid "5" +msgstr "5" + +#: GUI/widgets/settingsPage.py:1950 GUI/widgets/settingsPage.py:1950 +msgid "5 GB" +msgstr "5 GB" + +#: GUI/widgets/settingsPage.py:1950 +msgid "50 GB" +msgstr "50 GB" + +#: GUI/widgets/settingsPage.py:1619 +msgid "6" +msgstr "6" + +#: GUI/widgets/settingsPage.py:1728 GUI/widgets/settingsPage.py:1728 +msgid "64 kbps" +msgstr "64 kbps" + +#: GUI/widgets/settingsPage.py:1515 +msgid "75%" +msgstr "75%" + +#: GUI/widgets/settingsPage.py:1619 +msgid "8" +msgstr "8" + +#: GUI/widgets/settingsPage.py:1728 +msgid "80 kbps" +msgstr "80 kbps" + +#: GUI/widgets/settingsPage.py:1515 +msgid "90%" +msgstr "90%" + +#: GUI/widgets/settingsPage.py:1716 GUI/widgets/settingsPage.py:1728 +msgid "96 kbps" +msgstr "96 kbps" + +msgid "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC.When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" +msgstr "Audio immer mit 44,1 kHz (CD-Rate) ausgeben. Empfohlen für frühe iPods (1G-4G), die Probleme mit 48-kHz-ALAC haben können. Wenn aus, wird die Abtastrate auf 48 kHz reduziert, da iPods nur 48 kHz oder niedriger decodieren können." + +#: GUI/widgets/settingsPage.py:1640 +msgid "Analyze loudness of files missing ReplayGain/iTunNORM tags using ffmpeg, then write the result back into your PC files and sync to iPod. Sound Check values are always synced to iPod regardless of this setting." +msgstr "Analysiert die Lautheit von Dateien ohne ReplayGain/iTunNORM-Tags mit ffmpeg, schreibt das Ergebnis in die PC-Dateien zurück und synchronisiert es zum iPod. Sound-Check-Werte werden unabhängig davon immer zum iPod synchronisiert." + +#: GUI/widgets/settingsPage.py:1702 +msgid "Audio quality preset for music tracks. High: 256 kbps / best VBR. Balanced: 192 kbps. Compact: 128 kbps / smaller VBR." +msgstr "Audioqualitätsprofil für Musiktitel. Hoch: 256 kbps / bestes VBR. Ausgewogen: 192 kbps. Kompakt: 128 kbps / kleineres VBR." + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 GUI/widgets/settingsPage.py:1627 GUI/widgets/settingsPage.py:1688 GUI/widgets/settingsPage.py:1688 GUI/widgets/settingsPage.py:1787 GUI/widgets/settingsPage.py:1787 +msgid "Auto" +msgstr "Automatisch" + +#: GUI/widgets/settingsPage.py:1902 +msgid "Automatically scrobble new iPod plays to connected services when you sync." +msgstr "Neue iPod-Wiedergaben beim Synchronisieren automatisch an verbundene Dienste scrobbeln." + +#: GUI/widgets/settingsPage.py:1658 +msgid "Average" +msgstr "Durchschnitt" + +#: GUI/widgets/settingsPage.py:2001 +msgid "Backup Before Sync" +msgstr "Backup vor Synchronisierung" + +#: GUI/widgets/settingsPage.py:1995 +msgid "Backup Location" +msgstr "Backup-Speicherort" + +#: GUI/widgets/settingsPage.py:1702 GUI/widgets/settingsPage.py:1702 +msgid "Balanced" +msgstr "Ausgewogen" + +#: GUI/widgets/settingsPage.py:1522 GUI/widgets/settingsPage.py:1522 +msgid "Blue (Default)" +msgstr "Blau (Standard)" + +#: GUI/widgets/settingsPage.py:1709 GUI/widgets/settingsPage.py:1709 +msgid "CBR" +msgstr "CBR" + +#: GUI/widgets/settingsPage.py:1709 +msgid "CBR uses a fixed target bitrate. VBR targets a quality level and lets the encoder choose bitrate per frame — typically better quality per byte." +msgstr "CBR verwendet eine feste Zielbitrate. VBR zielt auf eine Qualitätsstufe und lässt den Encoder die Bitrate pro Frame wählen — meist bessere Qualität pro Byte." + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Frappé" +msgstr "Catppuccin Frappé" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Latte" +msgstr "Catppuccin Latte" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Macchiato" +msgstr "Catppuccin Macchiato" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Mocha" +msgstr "Catppuccin Mocha" + +#: GUI/widgets/settingsPage.py:2001 +msgid "Choose whether sync creates a full device backup automatically, asks each time, or skips pre-sync backups." +msgstr "Legt fest, ob die Synchronisierung automatisch ein vollständiges Gerätebackup erstellt, jedes Mal fragt oder Backups vor der Synchronisierung überspringt." + +msgid "Choose which lossy encoder to use.Auto chooses the best available." +msgstr "Wähle den verlustbehafteten Encoder. Automatisch wählt den besten verfügbaren." + +#: GUI/widgets/settingsPage.py:1702 +msgid "Compact" +msgstr "Kompakt" + +#: GUI/widgets/settingsPage.py:1918 +msgid "Connect your Last.fm account to scrobble iPod plays. You will need to provide your Last.fm API Key and API Secret." +msgstr "Verbinde dein Last.fm-Konto, um iPod-Wiedergaben zu scrobbeln. Du musst Last.fm-API-Schlüssel und API-Secret angeben." + +#: GUI/widgets/settingsPage.py:1908 +msgid "Connect your ListenBrainz account to scrobble iPod plays. Copy your user token from the link below." +msgstr "Verbinde dein ListenBrainz-Konto, um iPod-Wiedergaben zu scrobbeln. Kopiere deinen Benutzer-Token über den Link unten." + +#: GUI/widgets/settingsPage.py:1495 GUI/widgets/settingsPage.py:1495 +msgid "Dark" +msgstr "Dunkel" + +#: GUI/widgets/settingsPage.py:1769 +msgid "Downmix to mono when encoding podcasts and audiobooks. Mono at lowbites sounds significantly better than stereo and cuts file sizes in half." +msgstr "Beim Codieren von Podcasts und Hörbüchern auf Mono heruntermischen. Mono klingt bei niedrigen Bitraten deutlich besser als Stereo und halbiert die Dateigröße." + +msgid "Enable separate quality settings for podcasts and audiobooks.Music tracks are unaffected." +msgstr "Aktiviert separate Qualitätseinstellungen für Podcasts und Hörbücher. Musiktitel bleiben unverändert." + +#: GUI/widgets/settingsPage.py:1795 +msgid "Enables a quality-enhancement post-processing pass in libfdk_aac. Improves output at the cost of slightly longer encode times. Only affects the libfdk_aac encoder." +msgstr "Enables a quality-enhancement post-processing pass in libfdk_aac. Improves output at the cost of slightly longer encode times. Only affects the libfdk_aac encoder." + +#: GUI/widgets/settingsPage.py:1735 +msgid "Encode lossless sources (ALAC, FLAC, WAV, AIFF) as your selected lossy format instead of ALAC. Saves iPod storage at the cost of quality." +msgstr "Codiert verlustfreie Quellen (ALAC, FLAC, WAV, AIFF) statt ALAC in das gewählte verlustbehaftete Format. Spart iPod-Speicher auf Kosten der Qualität." + +#: GUI/widgets/settingsPage.py:1813 +msgid "Encodes stereo as Sum (Mid) and Difference (Side) rather than Left/Right. Concentrates bits where the signal is strongest, particularly on centred content. Affects the native aac encoder only." +msgstr "Encodes stereo as Sum (Mid) and Difference (Side) rather than Left/Right. Concentrates bits where the signal is strongest, particularly on centred content. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1647 +msgid "For portrait-heavy photos, rotate the device viewing caches clockwise when that uses more of the iPod's landscape photo screen. The original PC files are not modified." +msgstr "Bei vielen Hochformatfotos werden die Anzeige-Caches auf dem Gerät im Uhrzeigersinn gedreht, wenn dadurch mehr vom Querformat-Bildschirm des iPod genutzt wird. Die Originaldateien auf dem PC werden nicht verändert." + +#: GUI/widgets/settingsPage.py:1741 +msgid "Force existing MP3 and AAC files through the selected lossy encoder. Use this to make the synced library more uniform or smaller." +msgstr "Erzwingt, dass vorhandene MP3- und AAC-Dateien durch den gewählten verlustbehafteten Encoder laufen. Nützlich für eine einheitlichere oder kleinere synchronisierte Mediathek." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Gold" +msgstr "Gold" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Green" +msgstr "Grün" + +#: GUI/widgets/settingsPage.py:1702 +msgid "High Quality" +msgstr "Hohe Qualität" + +#: GUI/widgets/settingsPage.py:1658 +msgid "Highest" +msgstr "Höchste" + +#: GUI/widgets/settingsPage.py:1658 +msgid "How to resolve rating conflicts when iPod and PC ratings differ. iPod/PC Wins uses that source (falling back to the other if zero). Highest/Lowest picks the max/min non-zero value. Average rounds to the nearest star." +msgstr "Legt fest, wie Bewertungskonflikte zwischen iPod und PC gelöst werden. iPod/PC gewinnt nutzt diese Quelle (bei 0 mit Rückfall auf die andere). Höchste/Niedrigste wählt den maximalen/minimalen Nicht-Null-Wert. Durchschnitt rundet auf den nächsten Stern." + +#: GUI/widgets/settingsPage.py:1572 +msgid "Ko-fi" +msgstr "Ko-fi" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Light" +msgstr "Hell" + +#: GUI/widgets/settingsPage.py:1658 +msgid "Lowest" +msgstr "Niedrigste" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Match iPod" +msgstr "An iPod anpassen" + +#: GUI/widgets/settingsPage.py:2008 +msgid "Max Backups" +msgstr "Max. Backups" + +#: GUI/widgets/settingsPage.py:1787 +msgid "Maximum frequency the encoder will output. Lowering this (16–18 kHz) frees bits for the mid-range and can eliminate high-frequency squeaks at lower bitrates. Auto lets the encoder decide. Applies to all AAC encoders." +msgstr "Maximum frequency the encoder will output. Lowering this (16–18 kHz) frees bits for the mid-range and can eliminate high-frequency squeaks at lower bitrates. Auto lets the encoder decide. Applies to all AAC encoders." + +#: GUI/widgets/settingsPage.py:2008 +msgid "Maximum number of backup snapshots to keep per device. Oldest backups are automatically removed when the limit is exceeded." +msgstr "Maximale Anzahl an Backup-Snapshots pro Gerät. Älteste Backups werden automatisch entfernt, wenn das Limit überschritten wird." + +#: GUI/widgets/settingsPage.py:1819 +msgid "Merges high-frequency stereo bands into a single channel with direction metadata. Saves bits at low bitrates but can cause stereo image wobble. Affects the native aac encoder only." +msgstr "Merges high-frequency stereo bands into a single channel with direction metadata. Saves bits at low bitrates but can cause stereo image wobble. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1507 GUI/widgets/settingsPage.py:1507 +msgid "Off" +msgstr "Aus" + +#: GUI/widgets/settingsPage.py:1507 +msgid "On" +msgstr "Ein" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Orange" +msgstr "Orange" + +#: GUI/widgets/settingsPage.py:1619 +msgid "Overall concurrent sync work. This controls how many files can be prepared, fingerprinted, transcoded, or copied at once. Auto uses your CPU core count (capped at 8)." +msgstr "Gleichzeitige Gesamtarbeit bei der Synchronisierung. Steuert, wie viele Dateien gleichzeitig vorbereitet, fingerprinted, transcodiert oder kopiert werden. Automatisch nutzt die CPU-Kernanzahl (max. 8)." + +#: GUI/widgets/settingsPage.py:1658 +msgid "PC Wins" +msgstr "PC gewinnt" + +#: GUI/widgets/settingsPage.py:1893 +msgid "Path Overrides" +msgstr "Pfadüberschreibungen" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Pink" +msgstr "Pink" + +#: GUI/widgets/settingsPage.py:1882 +msgid "Point to a custom ffmpeg binary. Leave empty to auto-detect." +msgstr "Auf eine eigene ffmpeg-Binärdatei verweisen. Leer lassen für automatische Erkennung." + +#: GUI/widgets/settingsPage.py:1887 +msgid "Point to a custom fpcalc binary. Leave empty to auto-detect." +msgstr "Auf eine eigene fpcalc-Binärdatei verweisen. Leer lassen für automatische Erkennung." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Purple" +msgstr "Lila" + +#: GUI/widgets/settingsPage.py:1751 +msgid "Quality level for H.264 video transcodes. Lower CRF = better quality but larger files. Resolution and codec are always forced to iPod-compatible values." +msgstr "Qualitätsstufe für H.264-Videotranscodes. Niedrigerer CRF = bessere Qualität, aber größere Dateien. Auflösung und Codec werden immer iPod-kompatibel erzwungen." + +#: GUI/widgets/settingsPage.py:1722 +msgid "Quality level for VBR encoding. Higher = better quality, larger files." +msgstr "Qualitätsstufe für VBR-Codierung. Höher = bessere Qualität, größere Dateien." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Red" +msgstr "Rot" + +#: GUI/widgets/settingsPage.py:1807 +msgid "Replaces noise-like frequency bands with synthetic noise, saving bits. Can cause sandpaper or hissing artifacts if the encoder mistakes tonal content for noise. Off by default. Affects the native aac encoder only." +msgstr "Replaces noise-like frequency bands with synthetic noise, saving bits. Can cause sandpaper or hissing artifacts if the encoder mistakes tonal content for noise. Off by default. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1875 +msgid "Required for acoustic fingerprinting, which identifies tracks even after re-encoding." +msgstr "Erforderlich für akustisches Fingerprinting, das Titel auch nach erneuter Codierung erkennt." + +#: GUI/widgets/settingsPage.py:1869 +msgid "Required for transcoding and media probing. Includes ffmpeg and ffprobe." +msgstr "Erforderlich für Transcodierung und Medienanalyse. Enthält ffmpeg und ffprobe." + +#: GUI/widgets/settingsPage.py:1902 +msgid "Scrobble on Sync" +msgstr "Beim Synchronisieren scrobbeln" + +#: GUI/widgets/settingsPage.py:1928 +msgid "Services" +msgstr "Dienste" + +#: GUI/widgets/settingsPage.py:1801 +msgid "Shapes quantization noise around transients to reduce pre-echo (smearing before drum hits). Disabling saves a few bits per block. Affects the native aac encoder only." +msgstr "Shapes quantization noise around transients to reduce pre-echo (smearing before drum hits). Disabling saves a few bits per block. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1627 +msgid "Simultaneous writes to the iPod filesystem. Set to 1 for HDD-based iPods to reduce fragmentation risk. Auto uses an HDD-safe default when the device looks like a hard-drive iPod." +msgstr "Gleichzeitige Schreibvorgänge ins iPod-Dateisystem. Bei HDD-iPods auf 1 setzen, um Fragmentierung zu reduzieren. Automatisch nutzt einen HDD-sicheren Standard, wenn das Gerät wie ein Festplatten-iPod aussieht." + +#: GUI/widgets/settingsPage.py:1762 +msgid "Slower presets produce slightly better quality at the same CRF, but take much longer." +msgstr "Langsamere Presets erzeugen bei gleichem CRF etwas bessere Qualität, dauern aber deutlich länger." + +#: GUI/widgets/podcastBrowser.py:1566 +msgid "Subscriptions" +msgstr "Abonnements" + +#: GUI/widgets/settingsPage.py:1495 GUI/widgets/settingsPage.py:1507 +msgid "System" +msgstr "System" + +#: GUI/widgets/settingsPage.py:1716 +msgid "Target CBR bitrate for music tracks." +msgstr "Ziel-CBR-Bitrate für Musiktitel." + +#: GUI/widgets/settingsPage.py:1728 +msgid "Target bitrate for podcasts and audiobooks (Always uses CBR despite set Bitrate Mode). Pair with 'Mono for Spoken Word' for best results." +msgstr "Zielbitrate für Podcasts und Hörbücher (nutzt immer CBR unabhängig vom Bitratenmodus). Für beste Ergebnisse mit „Mono für Sprache“ kombinieren." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Teal" +msgstr "Türkis" + +#: GUI/widgets/settingsPage.py:1950 GUI/widgets/settingsPage.py:2008 +msgid "Unlimited" +msgstr "Unbegrenzt" + +#: GUI/widgets/settingsPage.py:1653 +msgid "Use aspect-fit for device photo thumbnail formats. When off (default), thumbnails use iTunes-style crop-to-fill." +msgstr "Für Geräte-Fotominiaturen „Einpassen“ verwenden. Wenn aus (Standard), nutzen Miniaturen iTunes-artiges Zuschneiden zum Füllen." + +#: GUI/widgets/settingsPage.py:1709 +msgid "VBR" +msgstr "VBR" + +#: GUI/widgets/settingsPage.py:1746 +msgid "When enabled, WAV files are converted to ALAC instead of copied. Prefer Lossy Encoding overrides this and converts WAV to the selected lossy format." +msgstr "Wenn aktiviert, werden WAV-Dateien statt Kopieren in ALAC umgewandelt. „Verlustbehaftete Codierung bevorzugen“ überschreibt dies und wandelt WAV ins gewählte verlustbehaftete Format um." + +#: GUI/widgets/settingsPage.py:1995 +msgid "Where full device backups are stored on your PC. Leave empty for the platform default." +msgstr "Speicherort vollständiger Gerätebackups auf deinem PC. Leer lassen für den Plattformstandard." + +#: GUI/widgets/settingsPage.py:1635 +msgid "While syncing, write ratings and sound check values into your PC music files. When off, no changes are made to your PC files." +msgstr "Schreibt beim Synchronisieren Bewertungen und Sound-Check-Werte in die Musikdateien auf dem PC. Wenn aus, werden PC-Dateien nicht verändert." + +#: GUI/widgets/settingsPage.py:1688 +msgid "aac" +msgstr "aac" + +#: GUI/widgets/settingsPage.py:1688 +msgid "aac_at" +msgstr "aac_at" + +#: GUI/widgets/settingsPage.py:1488 +msgid "en" +msgstr "en" + +#: GUI/widgets/settingsPage.py:1762 GUI/widgets/settingsPage.py:1762 +msgid "fast" +msgstr "fast" + +#: GUI/widgets/settingsPage.py:1658 GUI/widgets/settingsPage.py:1658 +msgid "iPod Wins" +msgstr "iPod gewinnt" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libfdk_aac" +msgstr "libfdk_aac" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libmp3lame" +msgstr "libmp3lame" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libshine" +msgstr "libshine" + +#: GUI/widgets/settingsPage.py:1762 +msgid "medium" +msgstr "medium" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q0 (Best)" +msgstr "q0 (Beste)" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q1" +msgstr "q1" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q2" +msgstr "q2" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q3" +msgstr "q3" + +#: GUI/widgets/settingsPage.py:1722 GUI/widgets/settingsPage.py:1722 +msgid "q4" +msgstr "q4" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q5" +msgstr "q5" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q6" +msgstr "q6" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q7" +msgstr "q7" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q8" +msgstr "q8" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q9 (Smallest)" +msgstr "q9 (Kleinste)" + +#: GUI/widgets/settingsPage.py:1762 +msgid "slow" +msgstr "slow" + +#: GUI/widgets/settingsPage.py:1762 +msgid "ultrafast" +msgstr "ultrafast" + +#: GUI/widgets/settingsPage.py:1762 +msgid "veryfast" +msgstr "veryfast" + +#: GUI/widgets/settingsPage.py:1780 +msgid "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC. When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" +msgstr "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC. When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" + +#: GUI/widgets/settingsPage.py:1688 +msgid "Choose which lossy encoder to use. Auto chooses the best available." +msgstr "Wähle den verlustbehafteten Encoder. Automatisch wählt den besten verfügbaren." + +#: GUI/widgets/settingsPage.py:1775 +msgid "Enable separate quality settings for podcasts and audiobooks. Music tracks are unaffected." +msgstr "Aktiviert separate Qualitätseinstellungen für Podcasts und Hörbücher. Musiktitel bleiben unverändert." + +#: GUI/widgets/playlistBrowser.py:1667 +msgid "New Smart Playlist" +msgstr "Neue intelligente Wiedergabeliste" + +#: GUI/widgets/podcastBrowser.py:1441 +msgid "Add Podcast" +msgstr "Podcast hinzufügen" + +#: GUI/widgets/podcastBrowser.py:1531 +msgid "Add Your First Podcast" +msgstr "Ersten Podcast hinzufügen" + +#: GUI/widgets/podcastBrowser.py:451 +msgid "Add this episode to iPod" +msgstr "Diese Folge zum iPod hinzufügen" + +#: GUI/widgets/podcastBrowser.py:469 GUI/widgets/podcastBrowser.py:449 GUI/widgets/podcastBrowser.py:2102 +msgid "Add to iPod" +msgstr "Zum iPod hinzufügen" + +#: GUI/widgets/podcastBrowser.py:1469 +msgid "Apply per-feed settings: remove listened/old episodes, fill empty slots with new episodes" +msgstr "Feed-Einstellungen anwenden: gehörte/alte Folgen entfernen und freie Plätze mit neuen Folgen füllen" + +#: GUI/widgets/podcastBrowser.py:235 +msgid "Downloaded" +msgstr "Heruntergeladen" + +#: GUI/widgets/podcastBrowser.py:2247 +msgid "Downloaded: {count}" +msgstr "Heruntergeladen: {count}" + +#: GUI/widgets/podcastBrowser.py:260 +msgid "Episode" +msgstr "Folge" + +#: GUI/widgets/podcastBrowser.py:2244 +msgid "Episodes: {count}" +msgstr "Folgen: {count}" + +#: GUI/widgets/podcastBrowser.py:516 GUI/widgets/podcastBrowser.py:596 GUI/widgets/podcastBrowser.py:525 +msgid "More" +msgstr "Mehr" + +#: GUI/widgets/podcastBrowser.py:1508 +msgid "No Podcast Subscriptions" +msgstr "Keine Podcast-Abonnements" + +#: GUI/widgets/podcastBrowser.py:586 +msgid "No description available." +msgstr "Keine Beschreibung verfügbar." + +#: GUI/widgets/podcastBrowser.py:233 +msgid "On iPod" +msgstr "Auf iPod" + +#: GUI/widgets/podcastBrowser.py:2250 +msgid "On iPod: {count}" +msgstr "Auf iPod: {count}" + +#: GUI/widgets/podcastBrowser.py:2240 +msgid "RSS feed linked" +msgstr "RSS-Feed verknüpft" + +#: GUI/widgets/podcastBrowser.py:1451 +msgid "Refresh All" +msgstr "Alle aktualisieren" + +#: GUI/widgets/podcastBrowser.py:2024 +msgid "Refresh Feed" +msgstr "Feed aktualisieren" + +#: GUI/widgets/podcastBrowser.py:2109 +msgid "Remove Download" +msgstr "Download entfernen" + +#: GUI/widgets/podcastBrowser.py:504 GUI/widgets/podcastBrowser.py:480 GUI/widgets/podcastBrowser.py:2116 +msgid "Remove from iPod" +msgstr "Vom iPod entfernen" + +#: GUI/widgets/podcastBrowser.py:484 +msgid "Remove this episode from iPod" +msgstr "Diese Folge vom iPod entfernen" + +#: GUI/widgets/podcastBrowser.py:1518 +msgid "" +"Search for podcasts or add an RSS feed to get started.\n" +"Episodes can be downloaded and synced to your iPod." +msgstr "" +"Suche nach Podcasts oder füge einen RSS-Feed hinzu, um loszulegen.\n" +"Folgen können heruntergeladen und mit deinem iPod synchronisiert werden." + +#: GUI/widgets/podcastBrowser.py:1648 GUI/widgets/podcastBrowser.py:2209 +msgid "Select a podcast" +msgstr "Podcast auswählen" + +#: GUI/widgets/podcastBrowser.py:596 GUI/widgets/podcastBrowser.py:526 +msgid "Show less" +msgstr "Weniger anzeigen" + +#: GUI/widgets/podcastBrowser.py:1461 +msgid "Sync Podcasts" +msgstr "Podcasts synchronisieren" + +#: GUI/widgets/podcastBrowser.py:2226 +msgid "Unknown Author" +msgstr "Unbekannter Autor" + +#: GUI/widgets/podcastBrowser.py:2026 +msgid "Unsubscribe" +msgstr "Abbestellen" + +#: GUI/widgets/podcastBrowser.py:637 GUI/widgets/podcastBrowser.py:584 +msgid "Untitled Episode" +msgstr "Unbenannte Folge" + +#: GUI/widgets/podcastBrowser.py:2225 +msgid "Untitled Podcast" +msgstr "Unbenannter Podcast" + +#: GUI/widgets/podcastBrowser.py:2238 +msgid "Updated {date}" +msgstr "Aktualisiert {date}" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Day" +msgstr "1 Tag" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Month" +msgstr "1 Monat" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Week" +msgstr "1 Woche" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "2 Months" +msgstr "2 Monate" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "2 Weeks" +msgstr "2 Wochen" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "3 Days" +msgstr "3 Tage" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "3 Months" +msgstr "3 Monate" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "Adding podcast…" +msgstr "Podcast wird hinzugefügt…" + +#: GUI/widgets/podcastBrowser.py:2547 +msgid "All podcasts are up to date" +msgstr "Alle Podcasts sind aktuell" + +#: GUI/widgets/podcastBrowser.py:2825 +msgid "All selected episodes are already on iPod" +msgstr "Alle ausgewählten Folgen sind bereits auf dem iPod" + +#: GUI/widgets/podcastBrowser.py:2581 +msgid "Already subscribed" +msgstr "Bereits abonniert" + +#: GUI/widgets/podcastSearchDialog.py:192 +msgid "Checking podcast results now." +msgstr "Podcast-Ergebnisse werden geprüft." + +#: GUI/widgets/podcastBrowser.py:2371 +msgid "Checking subscribed feeds for new episodes." +msgstr "Abonnierte Feeds werden auf neue Folgen geprüft." + +#: GUI/widgets/podcastBrowser.py:2671 +msgid "Checking the feed for the latest episodes." +msgstr "Feed wird auf neueste Folgen geprüft." + +#: GUI/widgets/podcastBrowser.py:1775 +msgid "Clear method:" +msgstr "Bereinigungsmethode:" + +#: GUI/widgets/podcastBrowser.py:1777 +msgid "Clear older than:" +msgstr "Älter löschen als:" + +#: GUI/widgets/podcastBrowser.py:1776 +msgid "Clear when listened:" +msgstr "Nach Anhören löschen:" + +#: GUI/widgets/podcastBrowser.py:2657 +msgid "Could not add podcast" +msgstr "Podcast konnte nicht hinzugefügt werden" + +#: GUI/widgets/podcastBrowser.py:2931 +msgid "Episodes not found on iPod" +msgstr "Folgen nicht auf dem iPod gefunden" + +#: GUI/widgets/podcastBrowser.py:1773 +msgid "Episodes:" +msgstr "Folgen:" + +#: GUI/widgets/podcastBrowser.py:2584 +msgid "Fetching feed…" +msgstr "Feed wird abgerufen…" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "Fetching the feed and latest episodes." +msgstr "Feed und neueste Folgen werden abgerufen." + +#: GUI/widgets/podcastBrowser.py:1774 +msgid "Fill with:" +msgstr "Auffüllen mit:" + +#: GUI/widgets/podcastSearchDialog.py:118 +msgid "Find podcasts" +msgstr "Podcasts suchen" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "Immediately" +msgstr "Sofort" + +#: GUI/widgets/podcastBrowser.py:2330 +msgid "Loading episodes…" +msgstr "Folgen werden geladen…" + +#: GUI/widgets/podcastBrowser.py:1765 +msgid "Mark for Replacement" +msgstr "Zum Ersetzen markieren" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "Never" +msgstr "Nie" + +#: GUI/widgets/podcastBrowser.py:1764 +msgid "Newest Episode" +msgstr "Neueste Folge" + +#: GUI/widgets/podcastBrowser.py:1764 +msgid "Next Episode" +msgstr "Nächste Folge" + +#: GUI/widgets/podcastBrowser.py:2281 +msgid "No episodes found" +msgstr "Keine Folgen gefunden" + +#: GUI/widgets/podcastBrowser.py:2815 +msgid "No episodes to sync" +msgstr "Keine Folgen zum Synchronisieren" + +#: GUI/widgets/podcastBrowser.py:2772 GUI/widgets/podcastBrowser.py:2899 +msgid "No iPod connected" +msgstr "Kein iPod verbunden" + +#: GUI/widgets/podcastSearchDialog.py:213 +msgid "No podcasts found" +msgstr "Keine Podcasts gefunden" + +#: GUI/widgets/podcastBrowser.py:2366 +msgid "No subscriptions to refresh" +msgstr "Keine Abonnements zum Aktualisieren" + +#: GUI/widgets/podcastBrowser.py:2476 +msgid "No subscriptions to sync" +msgstr "Keine Abonnements zum Synchronisieren" + +#: GUI/widgets/podcastBrowser.py:2446 +msgid "Podcasts could not refresh" +msgstr "Podcasts konnten nicht aktualisiert werden" + +#: GUI/widgets/podcastBrowser.py:2481 +msgid "Preparing podcast sync…" +msgstr "Podcast-Synchronisierung wird vorbereitet…" + +#: GUI/widgets/podcastBrowser.py:2457 +msgid "Refresh failed" +msgstr "Aktualisierung fehlgeschlagen" + +#: GUI/widgets/podcastBrowser.py:2481 +msgid "Refreshing feeds before building the sync plan." +msgstr "Feeds werden vor dem Erstellen des Synchronisierungsplans aktualisiert." + +#: GUI/widgets/podcastBrowser.py:2480 +msgid "Refreshing feeds for sync…" +msgstr "Feeds für Synchronisierung werden aktualisiert…" + +#: GUI/widgets/podcastBrowser.py:2371 +msgid "Refreshing podcasts…" +msgstr "Podcasts werden aktualisiert…" + +#: GUI/widgets/podcastBrowser.py:2671 +msgid "Refreshing this podcast…" +msgstr "Dieser Podcast wird aktualisiert…" + +#: GUI/widgets/podcastBrowser.py:1765 +msgid "Remove Immediately" +msgstr "Sofort entfernen" + +#: GUI/widgets/podcastSearchDialog.py:118 +msgid "Search by show name, or paste an RSS feed below." +msgstr "Nach Sendungsnamen suchen oder unten einen RSS-Feed einfügen." + +#: GUI/widgets/podcastSearchDialog.py:192 +msgid "Searching for podcasts…" +msgstr "Podcasts werden gesucht…" + +#: GUI/widgets/podcastBrowser.py:2756 +msgid "Select episodes first" +msgstr "Zuerst Folgen auswählen" + +#: GUI/widgets/podcastBrowser.py:2784 +msgid "Selected episodes are already on iPod" +msgstr "Ausgewählte Folgen sind bereits auf dem iPod" + +#: GUI/widgets/podcastBrowser.py:2786 +msgid "Selected episodes cannot be added yet" +msgstr "Ausgewählte Folgen können noch nicht hinzugefügt werden" + +#: GUI/widgets/podcastBrowser.py:2448 +msgid "Some podcasts could not refresh" +msgstr "Einige Podcasts konnten nicht aktualisiert werden" + +#: GUI/widgets/podcastBrowser.py:2196 +msgid "Subscribed shows will appear here after their feeds refresh." +msgstr "Abonnierte Sendungen erscheinen hier nach der Feed-Aktualisierung." + +#: GUI/widgets/podcastBrowser.py:2569 +msgid "Sync failed" +msgstr "Synchronisierung fehlgeschlagen" + +#: GUI/widgets/podcastBrowser.py:2471 GUI/widgets/podcastBrowser.py:2769 +msgid "This iPod does not support podcasts" +msgstr "Dieser iPod unterstützt keine Podcasts" + +#: GUI/widgets/podcastBrowser.py:2281 +msgid "This podcast loaded, but its feed did not list any episodes." +msgstr "Dieser Podcast wurde geladen, aber der Feed enthält keine Folgen." + +#: GUI/widgets/podcastSearchDialog.py:213 +msgid "Try a different show name, or paste the RSS feed directly." +msgstr "Versuche einen anderen Sendungsnamen oder füge den RSS-Feed direkt ein." + +#: GUI/widgets/podcastBrowser.py:2196 +msgid "Waiting for episodes" +msgstr "Warte auf Folgen" + + +#: GUI/widgets/podcastBrowser.py:2895 GUI/widgets/podcastBrowser.py:2896 +msgid "Failed: {error}" +msgstr "Fehlgeschlagen: {error}" + +#: GUI/widgets/podcastBrowser.py:2593 GUI/widgets/podcastBrowser.py:2594 +msgid "Podcast sync: {summary}" +msgstr "Podcast-Synchronisierung: {summary}" + +#: GUI/widgets/podcastBrowser.py:2425 +msgid "Refreshed {count} feed" +msgstr "{count} Feed aktualisiert" + +#: GUI/widgets/podcastBrowser.py:2427 +msgid "Refreshed {count} feeds" +msgstr "{count} Feeds aktualisiert" + +#: GUI/widgets/podcastBrowser.py:2454 +msgid "Refreshed {count}; {failures} feed could not update" +msgstr "{count} aktualisiert; {failures} Feed konnte nicht aktualisiert werden" + +#: GUI/widgets/podcastBrowser.py:2459 +msgid "Refreshed {count}; {failures} feeds could not update" +msgstr "{count} aktualisiert; {failures} Feeds konnten nicht aktualisiert werden" + +#: GUI/widgets/podcastBrowser.py:2745 GUI/widgets/podcastBrowser.py:2746 +msgid "Refreshed {title}" +msgstr "{title} aktualisiert" + +#: GUI/widgets/podcastBrowser.py:2374 +msgid "Refreshing {count} feeds…" +msgstr "Aktualisiere {count} Feeds…" + +#: GUI/widgets/podcastBrowser.py:2372 +msgid "Refreshing {count} feed…" +msgstr "Aktualisiere {count} Feed…" + +#: GUI/widgets/podcastBrowser.py:2715 GUI/widgets/podcastBrowser.py:2716 +msgid "Refreshing {title}…" +msgstr "Aktualisiere {title}…" + +#: GUI/widgets/podcastBrowser.py:2942 +msgid "Removed {count} download" +msgstr "{count} Download entfernt" + +#: GUI/widgets/podcastBrowser.py:2944 +msgid "Removed {count} downloads" +msgstr "{count} Downloads entfernt" + +#: GUI/widgets/podcastBrowser.py:2881 +msgid "Sending {count} episode to sync…" +msgstr "Sende {count} Folge zur Synchronisierung…" + +#: GUI/widgets/podcastBrowser.py:2883 +msgid "Sending {count} episodes to sync…" +msgstr "Sende {count} Folgen zur Synchronisierung…" + +#: GUI/widgets/podcastBrowser.py:3005 +msgid "Sending {count} removal to sync…" +msgstr "Sende {count} Entfernung zur Synchronisierung…" + +#: GUI/widgets/podcastBrowser.py:3007 +msgid "Sending {count} removals to sync…" +msgstr "Sende {count} Entfernungen zur Synchronisierung…" + +#: GUI/widgets/podcastBrowser.py:2675 GUI/widgets/podcastBrowser.py:2676 +msgid "Subscribed to {title}" +msgstr "{title} abonniert" + +#: GUI/widgets/podcastBrowser.py:2705 GUI/widgets/podcastBrowser.py:2706 +msgid "Unsubscribed from {title}" +msgstr "{title} abbestellt" + +#: GUI/widgets/podcastBrowser.py:2590 +msgid "{count} to add" +msgstr "{count} hinzuzufügen" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "{count} to remove" +msgstr "{count} zu entfernen" + +#: GUI/widgets/playlistBrowser.py +msgid "Music" +msgstr "Musik" + +#: GUI/widgets/playlistBrowser.py +msgid "Ringtones" +msgstr "Klingeltöne" + +#: GUI/widgets/playlistBrowser.py +msgid "Rentals" +msgstr "Leihfilme" diff --git a/locale/es/LC_MESSAGES/iopenpod.mo b/locale/es/LC_MESSAGES/iopenpod.mo new file mode 100644 index 0000000..bb15480 Binary files /dev/null and b/locale/es/LC_MESSAGES/iopenpod.mo differ diff --git a/locale/es/LC_MESSAGES/iopenpod.po b/locale/es/LC_MESSAGES/iopenpod.po new file mode 100644 index 0000000..17dbdcd --- /dev/null +++ b/locale/es/LC_MESSAGES/iopenpod.po @@ -0,0 +1,3025 @@ +#: GUI/widgets/photoBrowser.py:457 GUI/widgets/podcastBrowser.py:2330 +msgid "" +msgstr "" +"Project-Id-Version: iOpenPod\n" +"Report-Msgid-Bugs-To: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "" +"A crash report has been saved to:\n" +msgstr "" +"Se ha guardado un informe de fallo en:\n" + +#: GUI/widgets/settingsPage.py:885 +msgid "API Key" +msgstr "Clave API" + +#: GUI/widgets/settingsPage.py:976 +msgid "API Key and Secret required" +msgstr "Se requieren clave API y secreto" + +#: GUI/widgets/settingsPage.py:892 +msgid "API Secret" +msgstr "Secreto API" + +#: GUI/widgets/musicBrowser.py:336 +msgid "All Tracks" +msgstr "Todas las pistas" + +#: GUI/widgets/settingsPage.py:1608 +msgid "About" +msgstr "Acerca de" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Accent Color" +msgstr "Color de acento" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Advanced AAC" +msgstr "AAC avanzado" + +#: GUI/widgets/photoBrowser.py:439 +msgid "Albums" +msgstr "Álbumes" + +msgid "Album" +msgstr "Álbum" + +msgid "" +"An unexpected error occurred:\n" +"\n" +msgstr "" +"Se produjo un error inesperado:\n" +"\n" + +#: GUI/widgets/settingsPage.py:1608 +msgid "Appearance" +msgstr "Apariencia" + +#: GUI/widgets/settingsPage.py:1546 +msgid "Apply a subtle display-only sharpening pass to album art in grid cards and track lists. This does not modify artwork written to your iPod." +msgstr "Apply a subtle display-only sharpening pass to album art in grid cards and track lists. This does not modify artwork written to your iPod." + +msgid "Artists" +msgstr "Artistas" + +msgid "Artist" +msgstr "Artista" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Audio" +msgstr "Audio" + +msgid "Audiobooks" +msgstr "Audiolibros" + +#: GUI/widgets/settingsPage.py:588 GUI/widgets/settingsPage.py:530 GUI/widgets/settingsPage.py:598 +msgid "Auto-detect" +msgstr "Detectar automáticamente" + +msgid "Ask Each Time" +msgstr "Preguntar cada vez" + +#: GUI/widgets/settingsPage.py:1329 +msgid "Back" +msgstr "Atrás" + +msgid "Backup Folder" +msgstr "Carpeta de copias" + +#: GUI/widgets/settingsPage.py:2023 GUI/widgets/sidebar.py:906 +msgid "Backups" +msgstr "Copias de seguridad" + +msgid "Backups and Rollback" +msgstr "Copias y restauración" + +#: GUI/widgets/settingsPage.py:1787 +msgid "Bandwidth Cutoff" +msgstr "Corte de ancho de banda" + +msgid "Before Sync" +msgstr "Antes de sincronizar" + +#: GUI/widgets/settingsPage.py:1676 +msgid "Behavior" +msgstr "Comportamiento" + +msgid "Duration" +msgstr "Duración" + +#: GUI/widgets/settingsPage.py:1709 +msgid "Bitrate Mode" +msgstr "Modo de bitrate" + +#: GUI/widgets/settingsPage.py:1507 +msgid "Boost text and border contrast for accessibility. System follows your OS accessibility setting." +msgstr "Boost text and border contrast for accessibility. System follows your OS accessibility setting." + +msgid "Browse..." +msgstr "Explorar..." + +#: GUI/widgets/settingsPage.py:306 GUI/widgets/settingsPage.py:403 GUI/widgets/settingsPage.py:537 +msgid "Browse…" +msgstr "Explorar…" + +msgid "Cache Status" +msgstr "Estado de caché" + +msgid "Calculating…" +msgstr "Calculando…" + +#: GUI/app.py:2187 GUI/widgets/settingsPage.py:912 GUI/widgets/settingsPage.py:2959 +msgid "Cancel" +msgstr "Cancelar" + +#: GUI/widgets/settingsPage.py:1114 +msgid "Canceled" +msgstr "Cancelado" + +#: GUI/widgets/settingsPage.py:1554 GUI/widgets/settingsPage.py:2883 +msgid "Check" +msgstr "Comprobar" + +#: GUI/widgets/settingsPage.py:1554 +msgid "Check for a newer version of iOpenPod." +msgstr "Check for a newer version of iOpenPod." + +#: GUI/widgets/settingsPage.py:631 GUI/widgets/settingsPage.py:2877 +msgid "Checking…" +msgstr "Comprobando…" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Choose the color scheme for the interface. System follows your OS preference." +msgstr "Choose the color scheme for the interface. System follows your OS preference." + +#: GUI/widgets/settingsPage.py:1488 +msgid "Choose the interface language. Changing it rebuilds the window." +msgstr "Choose the interface language. Changing it rebuilds the window." + +#: GUI/widgets/settingsPage.py:1134 +msgid "Clear Cache" +msgstr "Borrar caché" + +#: GUI/widgets/settingsPage.py:1175 +msgid "Clear Transcode Cache" +msgstr "Borrar caché de transcodificación" + +#: GUI/widgets/sidebar.py:193 +msgid "Click to rename your iPod" +msgstr "Haz clic para cambiar el nombre del iPod" + +#: GUI/widgets/settingsPage.py:1640 +msgid "Compute Sound Check" +msgstr "Calcular Sound Check" + +#: GUI/widgets/settingsPage.py:744 GUI/widgets/settingsPage.py:796 GUI/widgets/settingsPage.py:901 GUI/widgets/settingsPage.py:962 GUI/widgets/settingsPage.py:3211 +msgid "Connect" +msgstr "Conectar" + +#: GUI/widgets/settingsPage.py:781 GUI/widgets/settingsPage.py:942 +msgid "Connected as" +msgstr "Conectado como" + +#: GUI/widgets/settingsPage.py:1746 +msgid "Convert WAV to ALAC" +msgstr "Convertir WAV a ALAC" + +#: GUI/widgets/settingsPage.py:1481 +msgid "Copy the current global values into this iPod's settings file." +msgstr "Copy the current global values into this iPod's settings file." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Customize the accent color used throughout the interface. Match iPod uses the body color of your connected iPod." +msgstr "Customize the accent color used throughout the interface. Match iPod uses the body color of your connected iPod." + +#: GUI/widgets/settingsPage.py:1176 +msgid "" +"Delete all cached transcoded files?\n" +"\n" +"They will be re-created on the next sync." +msgstr "" +"¿Borrar todos los archivos transcodificados en caché?\n" +"\n" +"Se volverán a crear en la próxima sincronización." + +#: GUI/widgets/settingsPage.py:1379 GUI/widgets/settingsPage.py:2055 +msgid "Device" +msgstr "Dispositivo" + +#: GUI/widgets/settingsPage.py:2055 +msgid "Device..." +msgstr "Dispositivo..." + +#: GUI/widgets/settingsPage.py:762 GUI/widgets/settingsPage.py:927 +msgid "Disconnect" +msgstr "Desconectar" + +#: GUI/app.py:2589 GUI/widgets/settingsPage.py:636 GUI/widgets/settingsPage.py:3115 GUI/widgets/settingsPage.py:3122 +msgid "Download" +msgstr "Descargar" + +#: GUI/widgets/settingsPage.py:671 GUI/widgets/settingsPage.py:672 +msgid "Downloading…" +msgstr "Descargando…" + +#: GUI/widgets/settingsPage.py:1893 +msgid "External Tools" +msgstr "Herramientas externas" + +#: GUI/widgets/settingsPage.py:1869 +msgid "FFmpeg" +msgstr "FFmpeg" + +#: GUI/widgets/settingsPage.py:1882 +msgid "FFmpeg Path Override" +msgstr "Ruta personalizada de FFmpeg" + +#: GUI/widgets/settingsPage.py:979 +msgid "Fetching token..." +msgstr "Obteniendo token..." + +#: GUI/widgets/settingsPage.py:1653 +msgid "Fit Thumbnails" +msgstr "Ajustar miniaturas" + +#: GUI/widgets/settingsPage.py:1515 +msgid "Font Size" +msgstr "Tamaño de fuente" + +#: GUI/widgets/settingsPage.py:1608 GUI/widgets/settingsPage.py:1928 +msgid "General" +msgstr "General" + +msgid "Genres" +msgstr "Géneros" + +#: GUI/widgets/settingsPage.py:862 +msgid "Get API keys ↗" +msgstr "Obtener claves API ↗" + +#: GUI/widgets/settingsPage.py:718 +msgid "Get token ↗" +msgstr "Obtener token ↗" + +#: GUI/widgets/settingsPage.py:1378 +msgid "Global" +msgstr "Global" + +#: GUI/widgets/sidebar.py:276 +msgid "Hours" +msgstr "Horas" + +#: GUI/widgets/settingsPage.py:1475 +msgid "Ignore this iPod's settings file and use the PC settings while this iPod is selected." +msgstr "Ignore this iPod's settings file and use the PC settings while this iPod is selected." + +#: GUI/widgets/settingsPage.py:1507 +msgid "Increased Contrast" +msgstr "Contraste aumentado" + +#: GUI/widgets/settingsPage.py:1819 +msgid "Intensity Stereo" +msgstr "Estéreo de intensidad" + +#: GUI/app.py:627 GUI/app.py:611 +msgid "Invalid iPod Folder" +msgstr "Carpeta de iPod no válida" + +#: GUI/widgets/settingsPage.py:1488 +msgid "Language" +msgstr "Idioma" + +#: GUI/widgets/settingsPage.py:1918 +msgid "Last.fm" +msgstr "Last.fm" + +#: GUI/widgets/sidebar.py:935 +msgid "Library" +msgstr "Biblioteca" + +msgid "LIBRARY" +msgstr "BIBLIOTECA" + +msgid "Library (Master)" +msgstr "Biblioteca (maestra)" + +#: GUI/widgets/settingsPage.py:1908 +msgid "ListenBrainz" +msgstr "ListenBrainz" + +#: GUI/widgets/settingsPage.py:2058 +msgid "Loading device settings..." +msgstr "Cargando ajustes del dispositivo..." + +#: GUI/app.py:482 +msgid "Loading iPod..." +msgstr "Cargando iPod..." + +msgid "Log Folder" +msgstr "Carpeta de registros" + +#: GUI/widgets/settingsPage.py:1688 +msgid "Lossy Encoder" +msgstr "Codificador con pérdida" + +#: GUI/widgets/settingsPage.py:1608 +msgid "Manage" +msgstr "Gestionar" + +msgid "Maximum Backups" +msgstr "Máximo de copias" + +#: GUI/widgets/settingsPage.py:1813 +msgid "Mid/Side Stereo" +msgstr "Estéreo medio/lateral" + +#: GUI/widgets/settingsPage.py:1769 +msgid "Mono for Spoken Word" +msgstr "Mono para voz hablada" + +msgid "Movies" +msgstr "Películas" + +#: GUI/widgets/settingsPage.py:1716 +msgid "Music Bitrate" +msgstr "Bitrate de música" + +#: GUI/widgets/settingsPage.py:1702 +msgid "Music Quality" +msgstr "Calidad de música" + +msgid "Music Videos" +msgstr "Videos musicales" + +#: GUI/widgets/podcastBrowser.py:1767 GUI/widgets/sidebar.py:41 +msgid "No" +msgstr "No" + +#: GUI/app.py:1418 GUI/app.py:1575 GUI/app.py:2087 GUI/widgets/sidebar.py:781 GUI/widgets/sidebar.py:189 GUI/widgets/sidebar.py:514 GUI/widgets/sidebar.py:437 GUI/widgets/sidebar.py:518 +msgid "No Device" +msgstr "Sin dispositivo" + +#: GUI/app.py:441 +msgid "" +"No device is currently selected.\n" +"Choose an iPod to access your library and sync tools." +msgstr "" +"No hay ningún dispositivo seleccionado.\n" +"Elige un iPod para acceder a la biblioteca y las herramientas de sincronización." + +#: GUI/widgets/sidebar.py:611 GUI/widgets/sidebar.py:573 +msgid "None" +msgstr "Ninguno" + +msgid "Name" +msgstr "Nombre" + +#: GUI/widgets/sidebar.py:915 +msgid "Normalize Tags" +msgstr "Normalizar etiquetas" + +#: GUI/widgets/settingsPage.py:1780 +msgid "Normalize to 44.1 kHz" +msgstr "Normalizar a 44,1 kHz" + +#: GUI/widgets/settingsPage.py:664 +msgid "Not found" +msgstr "No encontrado" + +#: GUI/widgets/settingsPage.py:299 GUI/widgets/settingsPage.py:366 +msgid "Not set" +msgstr "No configurado" + +#: GUI/widgets/settingsPage.py:1561 +msgid "Open" +msgstr "Abrir" + +#: GUI/widgets/settingsPage.py:1561 +msgid "Open the GitHub issue tracker to report problems or request features." +msgstr "Open the GitHub issue tracker to report problems or request features." + +#: GUI/widgets/settingsPage.py:289 GUI/widgets/settingsPage.py:382 +msgid "Open ↗" +msgstr "Abrir ↗" + +#: GUI/widgets/settingsPage.py:129 +msgid "Overridden by device settings" +msgstr "Anulado por los ajustes del dispositivo" + +#: GUI/widgets/settingsPage.py:1619 +msgid "Parallel Workers" +msgstr "Tareas paralelas" + +#: GUI/widgets/settingsPage.py:1627 +msgid "Parallel Writes" +msgstr "Escrituras paralelas" + +#: GUI/widgets/settingsPage.py:737 +msgid "Paste token here…" +msgstr "Pega el token aquí…" + +#: GUI/widgets/settingsPage.py:1807 +msgid "Perceptual Noise Substitution (PNS)" +msgstr "Perceptual Noise Substitution (PNS)" + +#: GUI/widgets/settingsPage.py:1676 +msgid "Performance" +msgstr "Rendimiento" + +msgid "Photos" +msgstr "Fotos" + +#: GUI/widgets/settingsPage.py:1943 GUI/widgets/settingsPage.py:1961 GUI/widgets/settingsPage.py:1968 +msgid "Platform default" +msgstr "Predeterminado de la plataforma" + +#: GUI/widgets/playlistBrowser.py:1353 +msgid "Playlists" +msgstr "Listas de reproducción" + +msgid "PODCAST PLAYLISTS" +msgstr "LISTAS DE PODCASTS" + +msgid "Please report this issue on GitHub." +msgstr "Informa de este problema en GitHub." + +msgid "Podcasts" +msgstr "Podcasts" + +#: GUI/widgets/settingsPage.py:1735 +msgid "Prefer Lossy Encoding" +msgstr "Preferir codificación con pérdida" + +#: GUI/widgets/sidebar.py:197 GUI/widgets/sidebar.py:784 +msgid "Press Select to choose your iPod" +msgstr "Pulsa Seleccionar para elegir tu iPod" + +#: GUI/widgets/sidebar.py:918 +msgid "Preview and apply iPod-friendly tag fixes across the whole library." +msgstr "Preview and apply iPod-friendly tag fixes across the whole library." + +#: GUI/widgets/settingsPage.py:1658 +msgid "Rating Conflict Strategy" +msgstr "Estrategia de conflicto de valoración" + +msgid "REGULAR PLAYLISTS" +msgstr "LISTAS NORMALES" + +#: GUI/app.py:488 +msgid "Reading library and device settings." +msgstr "Leyendo biblioteca y ajustes del dispositivo." + +#: GUI/widgets/settingsPage.py:1741 +msgid "Reencode Existing Lossy Files" +msgstr "Recodificar archivos con pérdida existentes" + +#: GUI/widgets/settingsPage.py:1561 +msgid "Report a Bug" +msgstr "Informar de un error" + +#: GUI/widgets/sidebar.py:873 +msgid "Rescan" +msgstr "Escanear" + +#: GUI/widgets/settingsPage.py:1481 +msgid "Reset" +msgstr "Restablecer" + +#: GUI/widgets/settingsPage.py:1481 +msgid "Reset Device Settings" +msgstr "Restablecer ajustes del dispositivo" + +#: GUI/widgets/settingsPage.py:553 +msgid "Reset to auto-detect" +msgstr "Restablecer a detección automática" + +msgid "Size" +msgstr "Tamaño" + +msgid "Sort" +msgstr "Ordenar" + +msgid "SMART PLAYLISTS" +msgstr "LISTAS INTELIGENTES" + +msgid "INTERNAL BROWSE CATEGORIES" +msgstr "CATEGORÍAS INTERNAS" + +#: GUI/widgets/settingsPage.py:419 +msgid "Reset to default" +msgstr "Restablecer predeterminado" + +#: GUI/widgets/settingsPage.py:1647 +msgid "Rotate Tall Photos on Device" +msgstr "Rotar fotos verticales en el dispositivo" + +#: GUI/widgets/settingsPage.py:1539 +msgid "Round album art corners in grid cards and track lists. This only changes how artwork is drawn in iOpenPod and does not modify anything written to your iPod." +msgstr "Round album art corners in grid cards and track lists. This only changes how artwork is drawn in iOpenPod and does not modify anything written to your iPod." + +#: GUI/widgets/settingsPage.py:1539 +msgid "Rounded Artwork" +msgstr "Carátulas redondeadas" + +#: GUI/widgets/sidebar.py:217 +msgid "Safely eject the iPod from your system" +msgstr "Expulsar el iPod de forma segura" + +#: GUI/widgets/sidebar.py:774 +msgid "Save failed" +msgstr "Error al guardar" + +#: GUI/widgets/sidebar.py:767 +msgid "Saved" +msgstr "Guardado" + +#: GUI/widgets/sidebar.py:761 +msgid "Saving…" +msgstr "Guardando…" + +#: GUI/widgets/settingsPage.py:1515 +msgid "Scale text size across the interface for accessibility." +msgstr "Scale text size across the interface for accessibility." + +#: GUI/widgets/sidebar.py:611 +msgid "Scheme 1" +msgstr "Esquema 1" + +#: GUI/widgets/sidebar.py:611 +msgid "Scheme 2" +msgstr "Esquema 2" + +#: GUI/widgets/settingsPage.py:1928 +msgid "Scrobbling" +msgstr "Scrobbling" + +#: GUI/widgets/sidebar.py:872 +msgid "Select" +msgstr "Elegir" + +#: GUI/app.py:451 +msgid "Select Device" +msgstr "Seleccionar dispositivo" + +#: GUI/widgets/settingsPage.py:579 +msgid "Select File" +msgstr "Seleccionar archivo" + +#: GUI/widgets/settingsPage.py:350 GUI/widgets/settingsPage.py:464 +msgid "Select Folder" +msgstr "Seleccionar carpeta" + +#: GUI/app.py:434 +msgid "Select an iPod to continue" +msgstr "Selecciona un iPod para continuar" + +#: GUI/widgets/settingsPage.py:2060 +msgid "Select an iPod to edit device settings" +msgstr "Selecciona un iPod para editar los ajustes del dispositivo" + +#: GUI/widgets/settingsPage.py:1335 GUI/widgets/sidebar.py:977 +msgid "Settings" +msgstr "Configuración" + +msgid "Settings Folder" +msgstr "Carpeta de ajustes" + +#: GUI/widgets/settingsPage.py:1546 +msgid "Sharpen Artwork" +msgstr "Enfocar carátulas" + +#: GUI/widgets/settingsPage.py:1534 +msgid "Show album art thumbnails next to tracks in the list view." +msgstr "Show album art thumbnails next to tracks in the list view." + +#: GUI/widgets/settingsPage.py:1775 +msgid "Smart Quality by Content Type" +msgstr "Calidad inteligente por tipo de contenido" + +#: GUI/widgets/sidebar.py:275 +msgid "Songs" +msgstr "Canciones" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Spoken Word" +msgstr "Voz hablada" + +#: GUI/widgets/settingsPage.py:1728 +msgid "Spoken Word Bitrate" +msgstr "Bitrate de voz hablada" + +#: GUI/widgets/settingsPage.py:1893 +msgid "Status" +msgstr "Estado" + +#: GUI/widgets/settingsPage.py:1976 GUI/widgets/sidebar.py:401 +msgid "Storage" +msgstr "Almacenamiento" + +msgid "Storage Paths" +msgstr "Rutas de almacenamiento" + +#: GUI/widgets/settingsPage.py:1572 +msgid "Support iOpenPod" +msgstr "Apoyar iOpenPod" + +#: GUI/widgets/settingsPage.py:1676 +msgid "Sync" +msgstr "Sincronización" + +#: GUI/widgets/sidebar.py:896 +msgid "Sync with PC" +msgstr "Sincronizar con el PC" + +msgid "TV Shows" +msgstr "Programas de TV" + +#: GUI/widgets/sidebar.py:284 +msgid "Technical Details" +msgstr "Detalles técnicos" + +#: GUI/widgets/settingsPage.py:1801 +msgid "Temporal Noise Shaping (TNS)" +msgstr "Modelado temporal de ruido (TNS)" + +#: GUI/app.py:612 +msgid "The selected folder could not be identified as an iPod." +msgstr "The selected folder could not be identified as an iPod." + +#: GUI/app.py:628 +msgid "" +"The selected folder does not appear to be a valid iPod root.\n" +"\n" +"Expected structure:\n" +" /iPod_Control/iTunes/\n" +"\n" +"Please select the root folder of your iPod." +msgstr "" +"The selected folder does not appear to be a valid iPod root.\n" +"\n" +"Expected structure:\n" +" /iPod_Control/iTunes/\n" +"\n" +"Please select the root folder of your iPod." + +#: GUI/widgets/settingsPage.py:1495 +msgid "Theme" +msgstr "Tema" + +#: GUI/widgets/settingsPage.py:1534 +msgid "Track List Artwork" +msgstr "Carátulas en lista de pistas" + +#: GUI/widgets/trackListTitleBar.py:141 +msgid "Tracks" +msgstr "Pistas" + +#: GUI/widgets/settingsPage.py:1976 +msgid "Transcode Cache" +msgstr "Caché de transcodificación" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Transcoding" +msgstr "Transcodificación" + +#: GUI/widgets/settingsPage.py:1475 +msgid "Use Global Settings" +msgstr "Usar ajustes globales" + +#: GUI/widgets/settingsPage.py:1722 +msgid "VBR Quality Level" +msgstr "Nivel de calidad VBR" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Video" +msgstr "Video" + +#: GUI/widgets/settingsPage.py:1762 +msgid "Video Encode Speed" +msgstr "Velocidad de codificación de video" + +#: GUI/widgets/settingsPage.py:1751 +msgid "Video Quality (CRF)" +msgstr "Calidad de video (CRF)" + +msgid "Videos" +msgstr "Videos" + +#: GUI/widgets/settingsPage.py:1016 +msgid "Waiting for browser approval..." +msgstr "Esperando aprobación del navegador..." + +#: GUI/widgets/settingsPage.py:1635 +msgid "Write Back to PC" +msgstr "Escribir de vuelta al PC" + +#: GUI/widgets/MBListView.py:168 GUI/widgets/MBListView.py:173 GUI/widgets/podcastBrowser.py:1767 GUI/widgets/sidebar.py:41 +msgid "Yes" +msgstr "Sí" + +#: GUI/widgets/settingsPage.py:1875 +msgid "fpcalc (Chromaprint)" +msgstr "fpcalc (Chromaprint)" + +#: GUI/widgets/settingsPage.py:1887 +msgid "fpcalc Path Override" +msgstr "fpcalc Path Override" + +msgid "iOpenPod Error" +msgstr "Error de iOpenPod" + +#: GUI/widgets/settingsPage.py:1572 +msgid "iOpenPod is and always will be completely free and open source. If you like it and would like to support me, it is so very appreciated." +msgstr "iOpenPod is and always will be completely free and open source. If you like it and would like to support me, it is so very appreciated." + +#: GUI/app.py:661 +msgid "iPod Not Writable" +msgstr "iPod no escribible" + +#: GUI/widgets/settingsPage.py:1795 +msgid "libfdk_aac Afterburner" +msgstr "libfdk_aac Afterburner" + +#: GUI/widgets/sidebar.py:675 GUI/widgets/sidebar.py:681 +msgid "{free:.1f} GB free of {total:.1f} GB" +msgstr "{free:.1f} GB libres de {total:.1f} GB" + +#: GUI/widgets/MBListView.py:2746 +msgid "(all columns shown)" +msgstr "(todas las columnas visibles)" + +msgid "{count:,} audiobook" +msgstr "{count:,} audiolibro" + +msgid "{count:,} audiobooks" +msgstr "{count:,} audiolibros" + +msgid "{count:,} episode" +msgstr "{count:,} episodio" + +msgid "{count:,} episodes" +msgstr "{count:,} episodios" + +msgid "{count:,} song" +msgstr "{count:,} canción" + +msgid "{count:,} songs" +msgstr "{count:,} canciones" + +msgid "{count:,} track" +msgstr "{count:,} pista" + +msgid "{count:,} tracks" +msgstr "{count:,} pistas" + +msgid "{count:,} video" +msgstr "{count:,} video" + +msgid "{count:,} videos" +msgstr "{count:,} videos" + +msgid "{shown:,} of {total:,} audiobook" +msgstr "{shown:,} of {total:,} audiobook" + +msgid "{shown:,} of {total:,} audiobooks" +msgstr "{shown:,} of {total:,} audiobooks" + +msgid "{shown:,} of {total:,} episode" +msgstr "{shown:,} de {total:,} episodio" + +msgid "{shown:,} of {total:,} episodes" +msgstr "{shown:,} de {total:,} episodios" + +msgid "{shown:,} of {total:,} song" +msgstr "{shown:,} de {total:,} canción" + +msgid "{shown:,} of {total:,} songs" +msgstr "{shown:,} de {total:,} canciones" + +msgid "{shown:,} of {total:,} track" +msgstr "{shown:,} de {total:,} pista" + +msgid "{shown:,} of {total:,} tracks" +msgstr "{shown:,} de {total:,} pistas" + +msgid "{shown:,} of {total:,} video" +msgstr "{shown:,} de {total:,} video" + +msgid "{shown:,} of {total:,} videos" +msgstr "{shown:,} de {total:,} videos" + +msgid "{gb:.2f} GB used of {max_gb:.0f} GB · {count:,} file" +msgstr "{gb:.2f} GB usados de {max_gb:.0f} GB · {count:,} archivo" + +msgid "{gb:.2f} GB used of {max_gb:.0f} GB · {count:,} files" +msgstr "{gb:.2f} GB usados de {max_gb:.0f} GB · {count:,} archivos" + +msgid "{gb:.2f} GB · {count:,} file" +msgstr "{gb:.2f} GB · {count:,} archivo" + +msgid "{gb:.2f} GB · {count:,} files" +msgstr "{gb:.2f} GB · {count:,} archivos" + +#: GUI/widgets/MBListView.py:2646 +msgid "Add Column" +msgstr "Añadir columna" + +msgid "Album Artist" +msgstr "Artista del álbum" + +msgid "Album ID" +msgstr "ID de álbum" + +msgid "Art Count" +msgstr "Número de carátulas" + +msgid "Art Formats:" +msgstr "Formatos de carátula:" + +msgid "Artist Ref" +msgstr "Ref. artista" + +msgid "Artwork" +msgstr "Carátula" + +msgid "Artwork Ref" +msgstr "Ref. carátula" + +msgid "Audio:" +msgstr "Audio:" + +msgid "Audio Quality" +msgstr "Calidad de audio" + +msgid "Bitrate" +msgstr "Bitrate" + +msgid "BPM" +msgstr "BPM" + +msgid "Bus/Format:" +msgstr "Bus/Format:" + +#: GUI/widgets/settingsPage.py:1943 +msgid "Cache Location" +msgstr "Ubicación de caché" + +#: GUI/widgets/sidebar.py:392 +msgid "Capabilities" +msgstr "Capacidades" + +msgid "Category" +msgstr "Categoría" + +msgid "Chapter Img:" +msgstr "Chapter Img:" + +msgid "Chapter Titles" +msgstr "Títulos de capítulos" + +msgid "Chapters" +msgstr "Capítulos" + +msgid "Cleared — {count:,} file removed" +msgstr "Borrado — {count:,} archivo eliminado" + +msgid "Cleared — {count:,} files removed" +msgstr "Borrado — {count:,} archivos eliminados" + +msgid "Comment" +msgstr "Comentario" + +msgid "Compilation" +msgstr "Recopilación" + +msgid "Composer" +msgstr "Compositor" + +msgid "Composer ID" +msgstr "Composer ID" + +msgid "Conflicts:" +msgstr "Conflictos:" + +#: GUI/widgets/musicBrowser.py:471 +msgid "Convert to a single chaptered track" +msgstr "Convertir en una sola pista con capítulos" + +msgid "Core Metadata" +msgstr "Metadatos principales" + +#: GUI/widgets/musicBrowser.py:560 +msgid "" +"Could not prepare artwork:\n" +"\n" +"{error}" +msgstr "" +"Could not prepare artwork:\n" +"\n" +"{error}" + +#: GUI/widgets/musicBrowser.py:582 +msgid "" +"Could not stage artwork update:\n" +"\n" +"{error}" +msgstr "" +"Could not stage artwork update:\n" +"\n" +"{error}" + +#: GUI/widgets/settingsPage.py:1961 +msgid "Custom directory to store iOpenPod settings. Useful for portable setups or backups." +msgstr "Custom directory to store iOpenPod settings. Useful for portable setups or backups." + +msgid "Date Added" +msgstr "Fecha añadida" + +msgid "Date Modified" +msgstr "Fecha modificada" + +msgid "Dates" +msgstr "Fechas" + +msgid "Description" +msgstr "Descripción" + +msgid "Device DB:" +msgstr "Device DB:" + +msgid "Disc #" +msgstr "Disco n.º" + +msgid "Disc Total" +msgstr "Total de discos" + +msgid "Disk Size:" +msgstr "Tamaño de disco:" + +msgid "Encoder" +msgstr "Codificador" + +msgid "Enclosure URL" +msgstr "Enclosure URL" + +msgid "Episode #" +msgstr "Episodio n.º" + +msgid "Episode ID" +msgstr "ID de episodio" + +msgid "Equalizer" +msgstr "Ecualizador" + +#: GUI/widgets/settingsPage.py:1194 +msgid "Error clearing cache: {error}" +msgstr "Error clearing cache: {error}" + +msgid "File Format" +msgstr "Formato de archivo" + +msgid "Flags" +msgstr "Marcas" + +msgid "Free Space:" +msgstr "Espacio libre:" + +msgid "Gapless" +msgstr "Gapless" + +msgid "Gapless Album" +msgstr "Gapless Album" + +msgid "Gapless Payload" +msgstr "Gapless Payload" + +msgid "Grouping" +msgstr "Agrupación" + +msgid "Has Lyrics" +msgstr "Tiene letra" + +msgid "Hash Scheme:" +msgstr "Hash Scheme:" + +#: GUI/widgets/MBListView.py:2628 +msgid "Hide \"{column}\"" +msgstr "Ocultar \"{column}\"" + +#: GUI/widgets/sidebar.py:364 +msgid "Identity" +msgstr "Identidad" + +msgid "Identifiers" +msgstr "Identificadores" + +msgid "Keywords" +msgstr "Palabras clave" + +msgid "Largest" +msgstr "Más grande" + +msgid "Last Played" +msgstr "Última reproducción" + +msgid "Last Skipped" +msgstr "Último salto" + +#: GUI/widgets/settingsPage.py:1976 +msgid "Locations" +msgstr "Ubicaciones" + +msgid "Locale" +msgstr "Configuración regional" + +msgid "Location" +msgstr "Ubicación" + +#: GUI/widgets/settingsPage.py:1968 +msgid "Log Location" +msgstr "Ubicación de registros" + +msgid "Lyrics" +msgstr "Letra" + +#: GUI/widgets/settingsPage.py:1950 +msgid "Max Cache Size" +msgstr "Tamaño máximo de caché" + +msgid "Max File:" +msgstr "Max File:" + +msgid "Max Transfer:" +msgstr "Max Transfer:" + +msgid "Media Type" +msgstr "Tipo de medio" + +#: GUI/widgets/trackListTitleBar.py:146 +msgid "Minimize" +msgstr "Minimizar" + +msgid "Model #:" +msgstr "Model #:" + +msgid "Most Albums" +msgstr "Más álbumes" + +msgid "Most Artists" +msgstr "Más artistas" + +msgid "Most Plays" +msgstr "Más reproducciones" + +msgid "Most Skipped" +msgstr "Más omitidas" + +msgid "Most Tracks" +msgstr "Más pistas" + +msgid "Network" +msgstr "Red" + +#: GUI/widgets/settingsPage.py:1950 +msgid "Oldest cached files are automatically removed (LRU) to stay within this limit. Set to Unlimited if storage is not a concern." +msgstr "Oldest cached files are automatically removed (LRU) to stay within this limit. Set to Unlimited if storage is not a concern." + +#: GUI/widgets/MBListView.py:2736 +msgid "Other" +msgstr "Otro" + +msgid "Photo Formats:" +msgstr "Formatos de foto:" + +msgid "Playback && Stats" +msgstr "Reproducción y estadísticas" + +msgid "Played" +msgstr "Reproducido" + +msgid "Plays" +msgstr "Reproducciones" + +msgid "Plays (iPod)" +msgstr "Reproducciones (iPod)" + +msgid "Podcast" +msgstr "Podcast" + +msgid "Podcasts:" +msgstr "Podcasts:" + +msgid "Post-gap" +msgstr "Post-gap" + +msgid "Pre-gap" +msgstr "Pre-gap" + +msgid "Product:" +msgstr "Product:" + +#: GUI/widgets/MBListView.py:3312 +msgid "Rating" +msgstr "Valoración" + +msgid "Release Date" +msgstr "Fecha de lanzamiento" + +msgid "Remember Pos." +msgstr "Recordar posición" + +#: GUI/widgets/MBListView.py:2640 +msgid "Resize All Columns to Fit" +msgstr "Ajustar todas las columnas" + +#: GUI/widgets/MBListView.py:2635 +msgid "Resize Column to Fit" +msgstr "Ajustar columna" + +msgid "RSS URL" +msgstr "URL RSS" + +msgid "Sample Count" +msgstr "Número de muestras" + +msgid "Sample Rate" +msgstr "Frecuencia de muestreo" + +#: GUI/widgets/gridHeaderBar.py:77 +msgid "Search…" +msgstr "Buscar…" + +msgid "Season" +msgstr "Temporada" + +msgid "Select an Album" +msgstr "Seleccionar un álbum" + +msgid "Select an Artist" +msgstr "Seleccionar un artista" + +msgid "Select a Genre" +msgstr "Seleccionar un género" + +msgid "Serial:" +msgstr "Serial:" + +#: GUI/widgets/settingsPage.py:1961 +msgid "Settings Location" +msgstr "Ubicación de ajustes" + +msgid "Shadow DB:" +msgstr "Shadow DB:" + +msgid "Show" +msgstr "Programa" + +msgid "Skip Shuffle" +msgstr "Omitir en aleatorio" + +msgid "Skips" +msgstr "Omisiones" + +#: GUI/widgets/gridHeaderBar.py:153 +msgid "Sort: {label} ▾" +msgstr "Ordenar: {label} ▾" + +msgid "Sort Album" +msgstr "Orden de álbum" + +msgid "Sort Album Artist" +msgstr "Orden de artista del álbum" + +msgid "Sort Artist" +msgstr "Orden de artista" + +msgid "Sort Composer" +msgstr "Orden de compositor" + +msgid "Sort Overrides" +msgstr "Anulaciones de orden" + +msgid "Sort Show" +msgstr "Orden de programa" + +msgid "Sort Title" +msgstr "Orden de título" + +msgid "Sparse Art:" +msgstr "Sparse Art:" + +msgid "Start Time" +msgstr "Hora de inicio" + +msgid "Stop Time" +msgstr "Hora de fin" + +msgid "Subtitle" +msgstr "Subtítulo" + +msgid "Time" +msgstr "Tiempo" + +msgid "Title" +msgstr "Título" + +msgid "Track #" +msgstr "Pista n.º" + +msgid "Track ID" +msgstr "ID de pista" + +msgid "Track Keywords" +msgstr "Palabras clave de pista" + +msgid "Track Total" +msgstr "Total de pistas" + +#: GUI/widgets/settingsPage.py:1167 +msgid "Unavailable ({error})" +msgstr "No disponible ({error})" + +#: GUI/widgets/musicBrowser.py:488 GUI/widgets/musicBrowser.py:559 GUI/widgets/musicBrowser.py:581 +msgid "Unify Artwork" +msgstr "Unificar carátulas" + +msgid "Updater ID:" +msgstr "Updater ID:" + +#: GUI/widgets/sidebar.py:374 +msgid "USB / SCSI" +msgstr "USB / SCSI" + +msgid "USB Parent:" +msgstr "USB Parent:" + +msgid "USB PID:" +msgstr "USB PID:" + +msgid "USB Serial:" +msgstr "USB Serial:" + +msgid "USB VID:" +msgstr "USB VID:" + +msgid "Voice Memos:" +msgstr "Voice Memos:" + +msgid "Volume Adj." +msgstr "Ajuste de volumen" + +#: GUI/widgets/settingsPage.py:1968 +msgid "Where iOpenPod writes log files and crash reports. Takes effect on next launch." +msgstr "Dónde iOpenPod guarda registros e informes de fallo. Se aplica en el próximo inicio." + +#: GUI/widgets/settingsPage.py:1943 +msgid "Where transcoded files are cached to avoid re-encoding on future syncs." +msgstr "Dónde se almacenan archivos transcodificados para evitar recodificarlos en futuras sincronizaciones." + +msgid "Year" +msgstr "Año" + +#: GUI/widgets/sidebar.py:546 +msgid "" +"{value}\n" +"Source: {source}" +msgstr "" +"{value}\n" +"Source: {source}" + +#: GUI/widgets/MBListView.py:3322 GUI/widgets/MBListView.py:3360 +msgid "(mixed selection)" +msgstr "(selección mixta)" + +#: GUI/widgets/MBListView.py:3102 +msgid "Add to Playlist" +msgstr "Añadir a lista" + +#: GUI/widgets/MBListView.py:3304 +msgid "Checked" +msgstr "Marcado" + +#: GUI/widgets/MBListView.py:3351 +msgid "Content Advisory" +msgstr "Aviso de contenido" + +#: GUI/widgets/MBListView.py:2364 +msgid "Content Advisory: Clean" +msgstr "Aviso de contenido: limpio" + +#: GUI/widgets/MBListView.py:2356 +msgid "Content Advisory: Explicit" +msgstr "Aviso de contenido: explícito" + +#: GUI/widgets/MBListView.py:3209 +msgid "Convert to Podcast" +msgstr "Convertir a podcast" + +#: GUI/widgets/MBListView.py:3191 +msgid "Copy as File(s)" +msgstr "Copiar como archivo(s)" + +#: GUI/widgets/MBListView.py:3188 +msgid "Copy as Text" +msgstr "Copiar como texto" + +#: GUI/widgets/settingsPage.py:2959 +msgid "Downloading update…" +msgstr "Descargando actualización…" + +#: GUI/widgets/MBListView.py:2908 +msgid "Edit ({count})" +msgstr "Editar ({count})" + +#: GUI/widgets/MBListView.py:153 GUI/widgets/MBListView.py:3367 +msgid "Explicit" +msgstr "Explícito" + +#: GUI/widgets/settingsPage.py:3060 +msgid "ffprobe missing" +msgstr "ffprobe missing" + +#: GUI/widgets/settingsPage.py:2961 +msgid "iOpenPod Update" +msgstr "iOpenPod Update" + +#: GUI/widgets/settingsPage.py:3214 +msgid "Invalid token" +msgstr "Token no válido" + +#: GUI/widgets/MBListView.py:3166 +msgid "Move Down" +msgstr "Mover abajo" + +#: GUI/widgets/MBListView.py:3162 +msgid "Move Up" +msgstr "Mover arriba" + +#: GUI/widgets/MBListView.py:3106 GUI/widgets/playlistBrowser.py:1331 GUI/widgets/playlistBrowser.py:1674 +msgid "New Playlist" +msgstr "Nueva lista" + +#: GUI/widgets/settingsPage.py:2948 +msgid "No Binary Available" +msgstr "No Binary Available" + +#: GUI/widgets/settingsPage.py:2949 +msgid "" +"No pre-built binary was found for your platform.\n" +"\n" +"Visit {release_page} to download manually." +msgstr "" +"No pre-built binary was found for your platform.\n" +"\n" +"Visit {release_page} to download manually." + +#: GUI/widgets/MBListView.py:3328 +msgid "No Rating" +msgstr "Sin valoración" + +#: GUI/widgets/MBListView.py:3366 +msgid "None (Unset)" +msgstr "Ninguno (sin definir)" + +msgid "Part of a compilation album" +msgstr "Parte de un álbum recopilatorio" + +msgid "Preparing {count} file…" +msgstr "Preparando {count} archivo…" + +msgid "Preparing {count} files…" +msgstr "Preparando {count} archivos…" + +msgid "Remove {count} Track from iPod" +msgstr "Eliminar {count} pista del iPod" + +msgid "Remove {count} Tracks from iPod" +msgstr "Eliminar {count} pistas del iPod" + +msgid "Remove {count} Track from Playlist" +msgstr "Eliminar {count} pista de la lista" + +msgid "Remove {count} Tracks from Playlist" +msgstr "Eliminar {count} pistas de la lista" + +#: GUI/widgets/MBListView.py:2752 +msgid "Reset Columns" +msgstr "Restablecer columnas" + +msgid "Skip When Shuffling" +msgstr "Omitir en reproducción aleatoria" + +msgid "Skip this track in shuffle mode" +msgstr "Omitir esta pista en modo aleatorio" + +#: GUI/widgets/MBListView.py:3074 +msgid "Split chapters into individual tracks" +msgstr "Dividir capítulos en pistas individuales" + +#: GUI/widgets/MBListView.py:773 +msgid "Starting drag…" +msgstr "Iniciando arrastre…" + +#: GUI/widgets/MBListView.py:3116 +msgid "Untitled" +msgstr "Sin título" + +#: GUI/widgets/settingsPage.py:2892 +msgid "Up to Date" +msgstr "Actualizado" + +#: GUI/widgets/settingsPage.py:2886 +msgid "Update Check Failed" +msgstr "Error al buscar actualizaciones" + +#: GUI/widgets/settingsPage.py:3180 +msgid "Validating…" +msgstr "Validando…" + +#: GUI/widgets/MBListView.py:3384 +msgid "Volume Adjustment" +msgstr "Ajuste de volumen" + +#: GUI/widgets/settingsPage.py:2893 +msgid "You are running the latest version (v{version})." +msgstr "You are running the latest version (v{version})." + +#: GUI/widgets/MBListView.py:155 GUI/widgets/MBListView.py:3368 +msgid "Clean" +msgstr "Limpio" + +#: GUI/widgets/sidebar.py:383 +msgid "Database" +msgstr "Base de datos" + +#: GUI/widgets/musicBrowser.py:463 GUI/widgets/playlistBrowser.py:419 +msgid "Edit" +msgstr "Editar" + +#: GUI/widgets/trackListTitleBar.py:150 +msgid "Maximize" +msgstr "Maximizar" + +#: GUI/widgets/MBListView.py:215 +msgid "{count} chapter" +msgstr "{count} capítulo" + +#: GUI/widgets/MBListView.py:216 +msgid "{count} chapters" +msgstr "{count} capítulos" + +#: GUI/app.py:2312 +msgid "" +"\n" +"\n" +"If you discard, the copied files will be cleaned up automatically the next time you sync." +msgstr "" +"\n" +"\n" +"If you discard, the copied files will be cleaned up automatically the next time you sync." + +#: app_core/jobs.py:70 +msgid " and " +msgstr " and " + +#: GUI/app.py:1439 GUI/app.py:1433 +msgid "Album Conversion" +msgstr "Conversión de álbum" + +#: GUI/app.py:1594 +msgid "Chapter Split" +msgstr "División de capítulos" + +#: GUI/app.py:1440 +msgid "Choose an album with at least two tracks." +msgstr "Elige un álbum con al menos dos pistas." + +#: GUI/app.py:781 +msgid "Could Not Load iPod" +msgstr "No se pudo cargar el iPod" + +#: GUI/app.py:1379 +msgid "" +"Could not download sync tools:\n" +"\n" +"{error}" +msgstr "" +"Could not download sync tools:\n" +"\n" +"{error}" + +#: GUI/app.py:1102 +msgid "" +"Could not save quick changes to iPod:\n" +"{error}\n" +"\n" +"iOpenPod is reloading the device view from the iPod." +msgstr "" +"Could not save quick changes to iPod:\n" +"{error}\n" +"\n" +"iOpenPod is reloading the device view from the iPod." + +#: GUI/app.py:955 app_core/jobs.py:1940 app_core/jobs.py:1989 app_core/jobs.py:2265 +msgid "Database write failed." +msgstr "Error al escribir la base de datos." + +#: GUI/app.py:958 +msgid "Device name updated successfully" +msgstr "Nombre del dispositivo actualizado" + +#: GUI/app.py:2317 +msgid "Discard" +msgstr "Descartar" + +#: GUI/app.py:1378 +msgid "Download Failed" +msgstr "Error de descarga" + +#: GUI/app.py:2625 GUI/widgets/podcastBrowser.py:237 +msgid "Downloading" +msgstr "Descargando" + +#: GUI/app.py:2638 +msgid "Downloading Tools…" +msgstr "Descargando herramientas…" + +#: GUI/app.py:1075 +msgid "Eject Failed" +msgstr "Error al expulsar" + +#: app_core/jobs.py:84 +msgid "" +"FFmpeg and ffprobe are required for transcoding and media probing.\n" +"Install from: https://ffmpeg.org" +msgstr "" +"FFmpeg and ffprobe are required for transcoding and media probing.\n" +"Install from: https://ffmpeg.org" + +#: GUI/app.py:1076 +msgid "" +"Failed to eject the iPod:\n" +"{error}" +msgstr "" +"Failed to eject the iPod:\n" +"{error}" + +#: GUI/app.py:966 +msgid "" +"Failed to rename iPod:\n" +"{error}" +msgstr "" +"Failed to rename iPod:\n" +"{error}" + +#: GUI/app.py:1423 GUI/app.py:1582 GUI/app.py:1184 +msgid "Library Loading" +msgstr "Cargando biblioteca" + +#: GUI/app.py:791 +msgid "Load an iPod library before running tag normalization." +msgstr "Load an iPod library before running tag normalization." + +#: GUI/app.py:2525 +msgid "Missing Tools" +msgstr "Herramientas faltantes" + +#: app_core/jobs.py:1922 app_core/jobs.py:1971 +msgid "No iPod connected." +msgstr "No hay ningún iPod conectado." + +#: app_core/jobs.py:1926 app_core/jobs.py:1975 +msgid "No iPod database loaded." +msgstr "No se cargó ninguna base de datos del iPod." + +#: GUI/app.py:2087 +msgid "No iPod device selected." +msgstr "No se seleccionó ningún iPod." + +#: GUI/app.py:824 +msgid "No iPod-specific metadata fixes were found for this library." +msgstr "No se encontraron correcciones de metadatos específicas del iPod para esta biblioteca." + +#: GUI/app.py:800 +msgid "No tracks were found in this iPod library." +msgstr "No se encontraron pistas en esta biblioteca del iPod." + +#: GUI/app.py:790 GUI/app.py:799 GUI/app.py:823 +msgid "Normalize iPod Tags" +msgstr "Normalizar etiquetas del iPod" + +#: GUI/app.py:2184 +msgid "Not Enough Space" +msgstr "No hay espacio suficiente" + +#: GUI/app.py:2575 +msgid "Not Now" +msgstr "Ahora no" + +#: GUI/app.py:2603 +msgid "OK" +msgstr "Aceptar" + +#: GUI/app.py:1418 GUI/app.py:1575 +msgid "Please select an iPod device first." +msgstr "Selecciona primero un iPod." + +#: GUI/app.py:1412 +msgid "Please wait for the current sync to finish before converting an album." +msgstr "Espera a que termine la sincronización actual antes de convertir un álbum." + +#: GUI/app.py:1030 +msgid "Please wait for the current sync to finish before ejecting." +msgstr "Espera a que termine la sincronización actual antes de expulsar." + +#: GUI/app.py:1569 +msgid "Please wait for the current sync to finish before splitting chapters." +msgstr "Espera a que termine la sincronización actual antes de dividir capítulos." + +#: GUI/app.py:1423 GUI/app.py:1583 GUI/app.py:1185 +msgid "Please wait for the iPod library to finish loading." +msgstr "Espera a que termine de cargar la biblioteca del iPod." + +#: GUI/app.py:2644 +msgid "Preparing download…" +msgstr "Preparando descarga…" + +#: GUI/app.py:1174 +msgid "Quick Changes Still Saving" +msgstr "Los cambios rápidos aún se están guardando" + +#: GUI/app.py:2421 +msgid "Reading dropped files..." +msgstr "Leyendo archivos soltados..." + +#: GUI/app.py:965 +msgid "Rename Failed" +msgstr "Error al renombrar" + +#: GUI/app.py:1101 +msgid "Save Failed" +msgstr "Error al guardar" + +#: GUI/app.py:979 +msgid "Save In Progress" +msgstr "Guardado en curso" + +#: GUI/app.py:2316 +msgid "Save Partial Database" +msgstr "Guardar base de datos parcial" + +#: GUI/app.py:2301 +msgid "Save Partial Sync?" +msgstr "¿Guardar sincronización parcial?" + +#: GUI/app.py:1008 +msgid "Still Reading iPod" +msgstr "Aún leyendo el iPod" + +#: GUI/app.py:2280 +msgid "Sync Error" +msgstr "Error de sincronización" + +#: GUI/app.py:2204 +msgid "Sync Failed" +msgstr "Sincronización fallida" + +#: GUI/app.py:1029 +msgid "Sync In Progress" +msgstr "Sincronización en curso" + +#: GUI/app.py:1411 GUI/app.py:1568 +msgid "Sync Running" +msgstr "Sincronización activa" + +#: GUI/app.py:2189 +msgid "Sync Until Full" +msgstr "Sincronizar hasta llenar" + +#: GUI/app.py:167 +msgid "Sync failed before making changes." +msgstr "La sincronización falló antes de realizar cambios." + +#: GUI/app.py:2185 +msgid "The selected sync is larger than the iPod's free space." +msgstr "La sincronización seleccionada supera el espacio libre del iPod." + +#: GUI/app.py:1393 +msgid "This iPod does not support podcasts." +msgstr "Este iPod no admite podcasts." + +#: GUI/app.py:2166 +msgid "" +"This sync is estimated to need more space than is available on the iPod.\n" +"\n" +"Available: {available}\n" +"Estimated needed: {required}\n" +"Estimated shortfall: {shortage}\n" +"\n" +"Sync Until Full will copy files in order until the next file would leave less than {reserve} free, then save the database with the items that actually synced." +msgstr "" +"This sync is estimated to need more space than is available on the iPod.\n" +"\n" +"Available: {available}\n" +"Estimated needed: {required}\n" +"Estimated shortfall: {shortage}\n" +"\n" +"Sync Until Full will copy files in order until the next file would leave less than {reserve} free, then save the database with the items that actually synced." + +#: GUI/app.py:746 GUI/app.py:748 +msgid "Unknown iPod" +msgstr "iPod desconocido" + +#: GUI/app.py:1392 +msgid "Unsupported iPod" +msgstr "iPod no compatible" + +#: GUI/app.py:2309 +msgid "Would you like to save these tracks to your iPod's database?" +msgstr "¿Quieres guardar estas pistas en la base de datos del iPod?" + +#: app_core/jobs.py:89 +msgid "" +"You can also set custom paths in\n" +"Settings -> External Tools." +msgstr "" +"También puedes configurar rutas personalizadas en\n" +"Configuración -> Herramientas externas." + +#: app_core/jobs.py:77 +msgid "" +"fpcalc is required for sync.\n" +"Install from: https://acoustid.org/chromaprint" +msgstr "" +"fpcalc is required for sync.\n" +"Install from: https://acoustid.org/chromaprint" + +#: GUI/app.py:2555 +msgid "" +"iOpenPod can download these automatically (~80 MB).\n" +"Download now?" +msgstr "" +"iOpenPod can download these automatically (~80 MB).\n" +"Download now?" + +#: GUI/app.py:186 +msgid "" +"iOpenPod could not load this iPod library.\n" +"\n" +"{error}" +msgstr "" +"iOpenPod could not load this iPod library.\n" +"\n" +"{error}" + +#: GUI/app.py:190 +msgid "" +"iOpenPod could not read this iPod cleanly.\n" +"\n" +"Mount path: {mount}\n" +"System error: {error}\n" +"\n" +"On Linux, this usually means the iPod mount is not accessible, the FAT filesystem is dirty, or the current user does not have permission to the mount.\n" +"\n" +"Try reconnecting the iPod. If it still fails, try remounting it read-write:\n" +" sudo mount -o remount,rw {quoted_mount}\n" +"\n" +"If the filesystem is dirty, unmount it before repairing it:\n" +" sudo umount {quoted_mount}\n" +" sudo fsck.vfat -a /dev/sdXN\n" +"\n" +"Replace /dev/sdXN with the iPod partition. Do not run fsck while the iPod is mounted." +msgstr "" +"iOpenPod could not read this iPod cleanly.\n" +"\n" +"Mount path: {mount}\n" +"System error: {error}\n" +"\n" +"On Linux, this usually means the iPod mount is not accessible, the FAT filesystem is dirty, or the current user does not have permission to the mount.\n" +"\n" +"Try reconnecting the iPod. If it still fails, try remounting it read-write:\n" +" sudo mount -o remount,rw {quoted_mount}\n" +"\n" +"If the filesystem is dirty, unmount it before repairing it:\n" +" sudo umount {quoted_mount}\n" +" sudo fsck.vfat -a /dev/sdXN\n" +"\n" +"Replace /dev/sdXN with the iPod partition. Do not run fsck while the iPod is mounted." + +#: GUI/app.py:1009 +msgid "iOpenPod is still finishing background reads from the iPod. Try ejecting again in a moment." +msgstr "iOpenPod is still finishing background reads from the iPod. Try ejecting again in a moment." + +#: GUI/app.py:1175 +msgid "iOpenPod is still saving pending quick changes. Please wait for {label} to finish before starting a full sync." +msgstr "iOpenPod is still saving pending quick changes. Please wait for {label} to finish before starting a full sync." + +#: GUI/app.py:980 +msgid "iOpenPod is still saving {label} to the iPod. Try ejecting again when the save finishes." +msgstr "iOpenPod is still saving {label} to the iPod. Try ejecting again when the save finishes." + +#: GUI/app.py:1050 +msgid "iPod Ejected" +msgstr "iPod expulsado" + +#: GUI/app.py:958 +msgid "iPod Renamed" +msgstr "iPod renombrado" + +#: GUI/app.py:1171 +msgid "quick changes" +msgstr "cambios rápidos" + +#: GUI/app.py:2290 +msgid "track" +msgstr "pista" + +#: GUI/app.py:2290 GUI/widgets/playlistBrowser.py:1251 +msgid "tracks" +msgstr "pistas" + +#: GUI/app.py:2293 +msgid "{count} more track was not copied." +msgstr "{count} pista más no se copió." + +#: GUI/app.py:2295 +msgid "{count} more tracks were not copied." +msgstr "{count} pistas más no se copiaron." + +#: GUI/app.py:2304 +msgid "{count} {tracks_word} were successfully copied to your iPod before the sync was cancelled." +msgstr "{count} {tracks_word} se copiaron correctamente al iPod antes de cancelar la sincronización." + +#: GUI/app.py:2544 +msgid "{tools} Not Found" +msgstr "{tools} no encontrado" + +#: GUI/widgets/photoBrowser.py:1694 +msgid "Add Photo to Album" +msgstr "Añadir foto al álbum" + +#: GUI/widgets/photoBrowser.py:1246 GUI/widgets/photoBrowser.py:480 +msgid "Add to Album" +msgstr "Añadir al álbum" + +#: GUI/widgets/photoBrowser.py:1676 +msgid "Album name:" +msgstr "Nombre del álbum:" + +#: GUI/widgets/photoBrowser.py:1695 +msgid "Album:" +msgstr "Álbum:" + +#: GUI/widgets/photoBrowser.py:814 +msgid "All Photos" +msgstr "Todas las fotos" + +#: GUI/widgets/playlistBrowser.py:1012 +msgid "Choose a playlist to inspect its tracks and database metadata" +msgstr "Elige una lista para inspeccionar sus pistas y metadatos" + +#: GUI/widgets/photoBrowser.py:1689 +msgid "Create another album first, or choose a photo that is not already in every album." +msgstr "Create another album first, or choose a photo that is not already in every album." + +#: GUI/widgets/playlistBrowser.py:437 +msgid "Delete" +msgstr "Eliminar" + +#: GUI/widgets/photoBrowser.py:1724 GUI/widgets/photoBrowser.py:1742 +msgid "Delete '{name}' from the iPod now?" +msgstr "Delete '{name}' from the iPod now?" + +#: GUI/widgets/photoBrowser.py:1323 GUI/widgets/photoBrowser.py:1723 +msgid "Delete Album" +msgstr "Eliminar álbum" + +#: GUI/widgets/photoBrowser.py:1267 GUI/widgets/photoBrowser.py:1741 GUI/widgets/photoBrowser.py:482 +msgid "Delete Photo" +msgstr "Eliminar foto" + +#: GUI/widgets/playlistBrowser.py:561 +msgid "Details" +msgstr "Detalles" + +#: GUI/widgets/playlistBrowser.py:451 GUI/widgets/playlistBrowser.py:1854 GUI/widgets/playlistBrowser.py:1879 +msgid "Evaluate Now" +msgstr "Evaluar ahora" + +#: GUI/widgets/photoBrowser.py:479 GUI/widgets/playlistBrowser.py:473 +msgid "Export" +msgstr "Exportar" + +#: GUI/widgets/photoBrowser.py:1660 +msgid "Export Album" +msgstr "Exportar álbum" + +#: GUI/widgets/photoBrowser.py:1296 +msgid "Export Album..." +msgstr "Exportar álbum..." + +#: GUI/widgets/photoBrowser.py:1660 +msgid "Export All Photos" +msgstr "Exportar todas las fotos" + +#: GUI/widgets/photoBrowser.py:1296 +msgid "Export All Photos..." +msgstr "Exportar todas las fotos..." + +#: GUI/widgets/photoBrowser.py:1634 +msgid "Export Photo" +msgstr "Exportar foto" + +#: GUI/widgets/photoBrowser.py:1238 +msgid "Export Photo..." +msgstr "Exportar foto..." + +#: GUI/widgets/playlistBrowser.py:1338 GUI/widgets/playlistBrowser.py:1974 GUI/widgets/playlistBrowser.py:1603 +msgid "Import Playlist" +msgstr "Importar lista" + +#: GUI/widgets/playlistBrowser.py:1401 +msgid "Importing Playlist…" +msgstr "Importando lista…" + +#: GUI/widgets/playlistBrowser.py:1603 +msgid "Importing…" +msgstr "Importando…" + +#: GUI/widgets/photoBrowser.py:423 GUI/widgets/photoBrowser.py:1305 GUI/widgets/photoBrowser.py:1676 +msgid "New Album" +msgstr "Nuevo álbum" + +#: GUI/widgets/photoBrowser.py:1710 +msgid "New album name:" +msgstr "Nombre del nuevo álbum:" + +#: GUI/widgets/photoBrowser.py:1688 +msgid "No Available Albums" +msgstr "No hay álbumes disponibles" + +#: GUI/widgets/photoBrowser.py:1574 GUI/widgets/photoBrowser.py:1650 +msgid "No Photos" +msgstr "No hay fotos" + +#: GUI/widgets/photoBrowser.py:1454 GUI/widgets/photoBrowser.py:1581 GUI/widgets/photoBrowser.py:1625 GUI/widgets/photoBrowser.py:1655 +msgid "No iPod Connected" +msgstr "No hay iPod conectado" + +#: GUI/widgets/photoBrowser.py:474 +msgid "No photo selected" +msgstr "No hay foto seleccionada" + +#: GUI/widgets/playlistBrowser.py:1177 +msgid "No playlists on this iPod" +msgstr "No hay listas en este iPod" + +#: GUI/widgets/playlistBrowser.py:1986 +msgid "Parsing playlist…" +msgstr "Analizando lista…" + +#: GUI/widgets/photoBrowser.py:481 +msgid "Remove from Album" +msgstr "Eliminar del álbum" + +#: GUI/widgets/photoBrowser.py:1256 +msgid "Remove from Current Album" +msgstr "Eliminar del álbum actual" + +#: GUI/widgets/photoBrowser.py:1710 GUI/widgets/photoBrowser.py:1316 +msgid "Rename Album" +msgstr "Renombrar álbum" + +#: GUI/widgets/playlistBrowser.py:528 +msgid "Rules" +msgstr "Reglas" + +#: GUI/widgets/photoBrowser.py:475 +msgid "Select a photo to inspect its preview and album details." +msgstr "Selecciona una foto para ver su vista previa y detalles." + +#: GUI/widgets/playlistBrowser.py:1513 GUI/widgets/playlistBrowser.py:1547 GUI/widgets/playlistBrowser.py:1811 GUI/widgets/playlistBrowser.py:384 GUI/widgets/playlistBrowser.py:1007 +msgid "Select a playlist" +msgstr "Seleccionar una lista" + +#: GUI/widgets/photoBrowser.py:1455 +msgid "Select an iPod before editing device photos." +msgstr "Selecciona un iPod antes de editar fotos del dispositivo." + +#: GUI/widgets/photoBrowser.py:1582 GUI/widgets/photoBrowser.py:1626 GUI/widgets/photoBrowser.py:1656 +msgid "Select an iPod before exporting device photos." +msgstr "Selecciona un iPod antes de exportar fotos del dispositivo." + +#: GUI/widgets/photoBrowser.py:1574 GUI/widgets/photoBrowser.py:1650 +msgid "There are no photos to export." +msgstr "No hay fotos para exportar." + +#: GUI/widgets/playlistBrowser.py:1838 +msgid "Writing…" +msgstr "Escribiendo…" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "1" +msgstr "1" + +#: GUI/widgets/settingsPage.py:1950 +msgid "1 GB" +msgstr "1 GB" + +#: GUI/widgets/settingsPage.py:2008 GUI/widgets/settingsPage.py:2008 +msgid "10" +msgstr "10" + +#: GUI/widgets/settingsPage.py:1950 +msgid "10 GB" +msgstr "10 GB" + +#: GUI/widgets/settingsPage.py:1515 GUI/widgets/settingsPage.py:1515 +msgid "100%" +msgstr "100%" + +#: GUI/widgets/settingsPage.py:1515 +msgid "110%" +msgstr "110%" + +#: GUI/widgets/settingsPage.py:1515 +msgid "125%" +msgstr "125%" + +#: GUI/widgets/settingsPage.py:1716 +msgid "128 kbps" +msgstr "128 kbps" + +#: GUI/widgets/settingsPage.py:1787 +msgid "15 kHz" +msgstr "15 kHz" + +#: GUI/widgets/settingsPage.py:1515 +msgid "150%" +msgstr "150%" + +#: GUI/widgets/settingsPage.py:1787 +msgid "16 kHz" +msgstr "16 kHz" + +#: GUI/widgets/settingsPage.py:1716 +msgid "160 kbps" +msgstr "160 kbps" + +#: GUI/widgets/settingsPage.py:1787 +msgid "17 kHz" +msgstr "17 kHz" + +#: GUI/widgets/settingsPage.py:1751 +msgid "18 (High)" +msgstr "18 (High)" + +#: GUI/widgets/settingsPage.py:1787 +msgid "18 kHz" +msgstr "18 kHz" + +#: GUI/widgets/settingsPage.py:1787 +msgid "19 kHz" +msgstr "19 kHz" + +#: GUI/widgets/settingsPage.py:1716 GUI/widgets/settingsPage.py:1716 +msgid "192 kbps" +msgstr "192 kbps" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "2" +msgstr "2" + +#: GUI/widgets/settingsPage.py:1950 +msgid "2 GB" +msgstr "2 GB" + +#: GUI/widgets/settingsPage.py:2008 +msgid "20" +msgstr "20" + +#: GUI/widgets/settingsPage.py:1751 +msgid "20 (Good)" +msgstr "20 (Good)" + +#: GUI/widgets/settingsPage.py:1950 +msgid "20 GB" +msgstr "20 GB" + +#: GUI/widgets/settingsPage.py:1787 +msgid "20 kHz" +msgstr "20 kHz" + +#: GUI/widgets/settingsPage.py:1716 +msgid "224 kbps" +msgstr "224 kbps" + +#: GUI/widgets/settingsPage.py:1751 GUI/widgets/settingsPage.py:1751 +msgid "23 (Balanced)" +msgstr "23 (Balanced)" + +#: GUI/widgets/settingsPage.py:1716 +msgid "256 kbps" +msgstr "256 kbps" + +#: GUI/widgets/settingsPage.py:1751 +msgid "26 (Low)" +msgstr "26 (Low)" + +#: GUI/widgets/settingsPage.py:1751 +msgid "28 (Very Low)" +msgstr "28 (Very Low)" + +#: GUI/widgets/settingsPage.py:1728 +msgid "32 kbps" +msgstr "32 kbps" + +#: GUI/widgets/settingsPage.py:1716 +msgid "320 kbps" +msgstr "320 kbps" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "4" +msgstr "4" + +#: GUI/widgets/settingsPage.py:1728 +msgid "48 kbps" +msgstr "48 kbps" + +#: GUI/widgets/settingsPage.py:2008 +msgid "5" +msgstr "5" + +#: GUI/widgets/settingsPage.py:1950 GUI/widgets/settingsPage.py:1950 +msgid "5 GB" +msgstr "5 GB" + +#: GUI/widgets/settingsPage.py:1950 +msgid "50 GB" +msgstr "50 GB" + +#: GUI/widgets/settingsPage.py:1619 +msgid "6" +msgstr "6" + +#: GUI/widgets/settingsPage.py:1728 GUI/widgets/settingsPage.py:1728 +msgid "64 kbps" +msgstr "64 kbps" + +#: GUI/widgets/settingsPage.py:1515 +msgid "75%" +msgstr "75%" + +#: GUI/widgets/settingsPage.py:1619 +msgid "8" +msgstr "8" + +#: GUI/widgets/settingsPage.py:1728 +msgid "80 kbps" +msgstr "80 kbps" + +#: GUI/widgets/settingsPage.py:1515 +msgid "90%" +msgstr "90%" + +#: GUI/widgets/settingsPage.py:1716 GUI/widgets/settingsPage.py:1728 +msgid "96 kbps" +msgstr "96 kbps" + +msgid "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC.When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" +msgstr "Emitir siempre audio a 44,1 kHz (calidad CD). Recomendado para iPods antiguos (1G-4G) que pueden tener problemas con ALAC a 48 kHz. Si está desactivado, la frecuencia se reduce a 48 kHz porque los iPods solo decodifican 48 kHz o menos." + +#: GUI/widgets/settingsPage.py:1640 +msgid "Analyze loudness of files missing ReplayGain/iTunNORM tags using ffmpeg, then write the result back into your PC files and sync to iPod. Sound Check values are always synced to iPod regardless of this setting." +msgstr "Analiza con ffmpeg la sonoridad de archivos sin etiquetas ReplayGain/iTunNORM, escribe el resultado en los archivos del PC y sincroniza al iPod. Los valores de Sound Check siempre se sincronizan al iPod." + +#: GUI/widgets/settingsPage.py:1702 +msgid "Audio quality preset for music tracks. High: 256 kbps / best VBR. Balanced: 192 kbps. Compact: 128 kbps / smaller VBR." +msgstr "Preset de calidad para música. Alta: 256 kbps / mejor VBR. Equilibrada: 192 kbps. Compacta: 128 kbps / VBR más pequeño." + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 GUI/widgets/settingsPage.py:1627 GUI/widgets/settingsPage.py:1688 GUI/widgets/settingsPage.py:1688 GUI/widgets/settingsPage.py:1787 GUI/widgets/settingsPage.py:1787 +msgid "Auto" +msgstr "Automático" + +#: GUI/widgets/settingsPage.py:1902 +msgid "Automatically scrobble new iPod plays to connected services when you sync." +msgstr "Registrar automáticamente nuevas reproducciones del iPod en servicios conectados al sincronizar." + +#: GUI/widgets/settingsPage.py:1658 +msgid "Average" +msgstr "Promedio" + +#: GUI/widgets/settingsPage.py:2001 +msgid "Backup Before Sync" +msgstr "Copia antes de sincronizar" + +#: GUI/widgets/settingsPage.py:1995 +msgid "Backup Location" +msgstr "Ubicación de copias" + +#: GUI/widgets/settingsPage.py:1702 GUI/widgets/settingsPage.py:1702 +msgid "Balanced" +msgstr "Equilibrado" + +#: GUI/widgets/settingsPage.py:1522 GUI/widgets/settingsPage.py:1522 +msgid "Blue (Default)" +msgstr "Azul (predeterminado)" + +#: GUI/widgets/settingsPage.py:1709 GUI/widgets/settingsPage.py:1709 +msgid "CBR" +msgstr "CBR" + +#: GUI/widgets/settingsPage.py:1709 +msgid "CBR uses a fixed target bitrate. VBR targets a quality level and lets the encoder choose bitrate per frame — typically better quality per byte." +msgstr "CBR usa un bitrate objetivo fijo. VBR apunta a un nivel de calidad y deja que el codificador elija el bitrate por fotograma, normalmente con mejor calidad por byte." + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Frappé" +msgstr "Catppuccin Frappé" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Latte" +msgstr "Catppuccin Latte" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Macchiato" +msgstr "Catppuccin Macchiato" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Mocha" +msgstr "Catppuccin Mocha" + +#: GUI/widgets/settingsPage.py:2001 +msgid "Choose whether sync creates a full device backup automatically, asks each time, or skips pre-sync backups." +msgstr "Elige si la sincronización crea una copia completa automáticamente, pregunta cada vez o omite las copias previas." + +msgid "Choose which lossy encoder to use.Auto chooses the best available." +msgstr "Elige qué codificador con pérdida usar. Automático elige el mejor disponible." + +#: GUI/widgets/settingsPage.py:1702 +msgid "Compact" +msgstr "Compacto" + +#: GUI/widgets/settingsPage.py:1918 +msgid "Connect your Last.fm account to scrobble iPod plays. You will need to provide your Last.fm API Key and API Secret." +msgstr "Conecta tu cuenta de Last.fm para registrar reproducciones del iPod. Debes proporcionar la clave API y el secreto API de Last.fm." + +#: GUI/widgets/settingsPage.py:1908 +msgid "Connect your ListenBrainz account to scrobble iPod plays. Copy your user token from the link below." +msgstr "Conecta tu cuenta de ListenBrainz para registrar reproducciones del iPod. Copia tu token de usuario desde el enlace inferior." + +#: GUI/widgets/settingsPage.py:1495 GUI/widgets/settingsPage.py:1495 +msgid "Dark" +msgstr "Oscuro" + +#: GUI/widgets/settingsPage.py:1769 +msgid "Downmix to mono when encoding podcasts and audiobooks. Mono at lowbites sounds significantly better than stereo and cuts file sizes in half." +msgstr "Mezcla a mono al codificar podcasts y audiolibros. A bitrates bajos, mono suena mucho mejor que estéreo y reduce el tamaño a la mitad." + +msgid "Enable separate quality settings for podcasts and audiobooks.Music tracks are unaffected." +msgstr "Activa ajustes de calidad separados para podcasts y audiolibros. La música no se ve afectada." + +#: GUI/widgets/settingsPage.py:1795 +msgid "Enables a quality-enhancement post-processing pass in libfdk_aac. Improves output at the cost of slightly longer encode times. Only affects the libfdk_aac encoder." +msgstr "Enables a quality-enhancement post-processing pass in libfdk_aac. Improves output at the cost of slightly longer encode times. Only affects the libfdk_aac encoder." + +#: GUI/widgets/settingsPage.py:1735 +msgid "Encode lossless sources (ALAC, FLAC, WAV, AIFF) as your selected lossy format instead of ALAC. Saves iPod storage at the cost of quality." +msgstr "Codifica fuentes sin pérdida (ALAC, FLAC, WAV, AIFF) en el formato con pérdida elegido en lugar de ALAC. Ahorra espacio del iPod a costa de calidad." + +#: GUI/widgets/settingsPage.py:1813 +msgid "Encodes stereo as Sum (Mid) and Difference (Side) rather than Left/Right. Concentrates bits where the signal is strongest, particularly on centred content. Affects the native aac encoder only." +msgstr "Encodes stereo as Sum (Mid) and Difference (Side) rather than Left/Right. Concentrates bits where the signal is strongest, particularly on centred content. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1647 +msgid "For portrait-heavy photos, rotate the device viewing caches clockwise when that uses more of the iPod's landscape photo screen. The original PC files are not modified." +msgstr "Para fotos principalmente verticales, rota las cachés de visualización del dispositivo si aprovecha mejor la pantalla horizontal del iPod. Los archivos originales del PC no se modifican." + +#: GUI/widgets/settingsPage.py:1741 +msgid "Force existing MP3 and AAC files through the selected lossy encoder. Use this to make the synced library more uniform or smaller." +msgstr "Fuerza archivos MP3 y AAC existentes a pasar por el codificador con pérdida elegido. Úsalo para una biblioteca sincronizada más uniforme o pequeña." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Gold" +msgstr "Dorado" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Green" +msgstr "Verde" + +#: GUI/widgets/settingsPage.py:1702 +msgid "High Quality" +msgstr "Alta calidad" + +#: GUI/widgets/settingsPage.py:1658 +msgid "Highest" +msgstr "Más alta" + +#: GUI/widgets/settingsPage.py:1658 +msgid "How to resolve rating conflicts when iPod and PC ratings differ. iPod/PC Wins uses that source (falling back to the other if zero). Highest/Lowest picks the max/min non-zero value. Average rounds to the nearest star." +msgstr "Define cómo resolver conflictos de valoración entre iPod y PC. Prioridad iPod/PC usa esa fuente (recurriendo a la otra si es cero). Más alta/más baja elige el valor no cero máximo/mínimo. Promedio redondea a la estrella más cercana." + +#: GUI/widgets/settingsPage.py:1572 +msgid "Ko-fi" +msgstr "Ko-fi" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Light" +msgstr "Claro" + +#: GUI/widgets/settingsPage.py:1658 +msgid "Lowest" +msgstr "Más baja" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Match iPod" +msgstr "Coincidir con iPod" + +#: GUI/widgets/settingsPage.py:2008 +msgid "Max Backups" +msgstr "Máximo de copias" + +#: GUI/widgets/settingsPage.py:1787 +msgid "Maximum frequency the encoder will output. Lowering this (16–18 kHz) frees bits for the mid-range and can eliminate high-frequency squeaks at lower bitrates. Auto lets the encoder decide. Applies to all AAC encoders." +msgstr "Maximum frequency the encoder will output. Lowering this (16–18 kHz) frees bits for the mid-range and can eliminate high-frequency squeaks at lower bitrates. Auto lets the encoder decide. Applies to all AAC encoders." + +#: GUI/widgets/settingsPage.py:2008 +msgid "Maximum number of backup snapshots to keep per device. Oldest backups are automatically removed when the limit is exceeded." +msgstr "Número máximo de instantáneas de copia por dispositivo. Las más antiguas se eliminan automáticamente al superar el límite." + +#: GUI/widgets/settingsPage.py:1819 +msgid "Merges high-frequency stereo bands into a single channel with direction metadata. Saves bits at low bitrates but can cause stereo image wobble. Affects the native aac encoder only." +msgstr "Merges high-frequency stereo bands into a single channel with direction metadata. Saves bits at low bitrates but can cause stereo image wobble. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1507 GUI/widgets/settingsPage.py:1507 +msgid "Off" +msgstr "Desactivado" + +#: GUI/widgets/settingsPage.py:1507 +msgid "On" +msgstr "Activado" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Orange" +msgstr "Naranja" + +#: GUI/widgets/settingsPage.py:1619 +msgid "Overall concurrent sync work. This controls how many files can be prepared, fingerprinted, transcoded, or copied at once. Auto uses your CPU core count (capped at 8)." +msgstr "Trabajo concurrente total de sincronización. Controla cuántos archivos se preparan, identifican, transcodifican o copian a la vez. Automático usa los núcleos de CPU (máx. 8)." + +#: GUI/widgets/settingsPage.py:1658 +msgid "PC Wins" +msgstr "Prioridad PC" + +#: GUI/widgets/settingsPage.py:1893 +msgid "Path Overrides" +msgstr "Rutas personalizadas" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Pink" +msgstr "Rosa" + +#: GUI/widgets/settingsPage.py:1882 +msgid "Point to a custom ffmpeg binary. Leave empty to auto-detect." +msgstr "Indica un binario ffmpeg personalizado. Déjalo vacío para detección automática." + +#: GUI/widgets/settingsPage.py:1887 +msgid "Point to a custom fpcalc binary. Leave empty to auto-detect." +msgstr "Indica un binario fpcalc personalizado. Déjalo vacío para detección automática." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Purple" +msgstr "Morado" + +#: GUI/widgets/settingsPage.py:1751 +msgid "Quality level for H.264 video transcodes. Lower CRF = better quality but larger files. Resolution and codec are always forced to iPod-compatible values." +msgstr "Nivel de calidad para transcodificaciones H.264. CRF más bajo = más calidad pero archivos más grandes. Resolución y códec siempre se fuerzan a valores compatibles con iPod." + +#: GUI/widgets/settingsPage.py:1722 +msgid "Quality level for VBR encoding. Higher = better quality, larger files." +msgstr "Nivel de calidad para codificación VBR. Más alto = más calidad, archivos más grandes." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Red" +msgstr "Rojo" + +#: GUI/widgets/settingsPage.py:1807 +msgid "Replaces noise-like frequency bands with synthetic noise, saving bits. Can cause sandpaper or hissing artifacts if the encoder mistakes tonal content for noise. Off by default. Affects the native aac encoder only." +msgstr "Replaces noise-like frequency bands with synthetic noise, saving bits. Can cause sandpaper or hissing artifacts if the encoder mistakes tonal content for noise. Off by default. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1875 +msgid "Required for acoustic fingerprinting, which identifies tracks even after re-encoding." +msgstr "Necesario para huella acústica, que identifica pistas incluso tras recodificar." + +#: GUI/widgets/settingsPage.py:1869 +msgid "Required for transcoding and media probing. Includes ffmpeg and ffprobe." +msgstr "Necesario para transcodificar y analizar medios. Incluye ffmpeg y ffprobe." + +#: GUI/widgets/settingsPage.py:1902 +msgid "Scrobble on Sync" +msgstr "Registrar al sincronizar" + +#: GUI/widgets/settingsPage.py:1928 +msgid "Services" +msgstr "Servicios" + +#: GUI/widgets/settingsPage.py:1801 +msgid "Shapes quantization noise around transients to reduce pre-echo (smearing before drum hits). Disabling saves a few bits per block. Affects the native aac encoder only." +msgstr "Shapes quantization noise around transients to reduce pre-echo (smearing before drum hits). Disabling saves a few bits per block. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1627 +msgid "Simultaneous writes to the iPod filesystem. Set to 1 for HDD-based iPods to reduce fragmentation risk. Auto uses an HDD-safe default when the device looks like a hard-drive iPod." +msgstr "Escrituras simultáneas en el sistema de archivos del iPod. Usa 1 para iPods con HDD para reducir fragmentación. Automático usa un valor seguro para HDD si el dispositivo parece un iPod con disco duro." + +#: GUI/widgets/settingsPage.py:1762 +msgid "Slower presets produce slightly better quality at the same CRF, but take much longer." +msgstr "Los presets más lentos dan algo más de calidad con el mismo CRF, pero tardan mucho más." + +#: GUI/widgets/podcastBrowser.py:1566 +msgid "Subscriptions" +msgstr "Suscripciones" + +#: GUI/widgets/settingsPage.py:1495 GUI/widgets/settingsPage.py:1507 +msgid "System" +msgstr "Sistema" + +#: GUI/widgets/settingsPage.py:1716 +msgid "Target CBR bitrate for music tracks." +msgstr "Bitrate CBR objetivo para música." + +#: GUI/widgets/settingsPage.py:1728 +msgid "Target bitrate for podcasts and audiobooks (Always uses CBR despite set Bitrate Mode). Pair with 'Mono for Spoken Word' for best results." +msgstr "Bitrate objetivo para podcasts y audiolibros (siempre usa CBR aunque se configure otro modo). Combínalo con “Mono para voz hablada” para mejores resultados." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Teal" +msgstr "Verde azulado" + +#: GUI/widgets/settingsPage.py:1950 GUI/widgets/settingsPage.py:2008 +msgid "Unlimited" +msgstr "Ilimitado" + +#: GUI/widgets/settingsPage.py:1653 +msgid "Use aspect-fit for device photo thumbnail formats. When off (default), thumbnails use iTunes-style crop-to-fill." +msgstr "Usa ajuste proporcional para miniaturas de fotos del dispositivo. Si está desactivado (predeterminado), las miniaturas usan recorte estilo iTunes." + +#: GUI/widgets/settingsPage.py:1709 +msgid "VBR" +msgstr "VBR" + +#: GUI/widgets/settingsPage.py:1746 +msgid "When enabled, WAV files are converted to ALAC instead of copied. Prefer Lossy Encoding overrides this and converts WAV to the selected lossy format." +msgstr "Si está activado, los WAV se convierten a ALAC en vez de copiarse. “Preferir codificación con pérdida” lo anula y convierte WAV al formato con pérdida elegido." + +#: GUI/widgets/settingsPage.py:1995 +msgid "Where full device backups are stored on your PC. Leave empty for the platform default." +msgstr "Dónde se guardan copias completas del dispositivo en el PC. Déjalo vacío para usar el valor predeterminado." + +#: GUI/widgets/settingsPage.py:1635 +msgid "While syncing, write ratings and sound check values into your PC music files. When off, no changes are made to your PC files." +msgstr "Durante la sincronización, escribe valoraciones y valores Sound Check en los archivos de música del PC. Si está desactivado, no modifica los archivos del PC." + +#: GUI/widgets/settingsPage.py:1688 +msgid "aac" +msgstr "aac" + +#: GUI/widgets/settingsPage.py:1688 +msgid "aac_at" +msgstr "aac_at" + +#: GUI/widgets/settingsPage.py:1488 +msgid "en" +msgstr "en" + +#: GUI/widgets/settingsPage.py:1762 GUI/widgets/settingsPage.py:1762 +msgid "fast" +msgstr "fast" + +#: GUI/widgets/settingsPage.py:1658 GUI/widgets/settingsPage.py:1658 +msgid "iPod Wins" +msgstr "Prioridad iPod" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libfdk_aac" +msgstr "libfdk_aac" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libmp3lame" +msgstr "libmp3lame" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libshine" +msgstr "libshine" + +#: GUI/widgets/settingsPage.py:1762 +msgid "medium" +msgstr "medium" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q0 (Best)" +msgstr "q0 (mejor)" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q1" +msgstr "q1" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q2" +msgstr "q2" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q3" +msgstr "q3" + +#: GUI/widgets/settingsPage.py:1722 GUI/widgets/settingsPage.py:1722 +msgid "q4" +msgstr "q4" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q5" +msgstr "q5" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q6" +msgstr "q6" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q7" +msgstr "q7" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q8" +msgstr "q8" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q9 (Smallest)" +msgstr "q9 (más pequeño)" + +#: GUI/widgets/settingsPage.py:1762 +msgid "slow" +msgstr "slow" + +#: GUI/widgets/settingsPage.py:1762 +msgid "ultrafast" +msgstr "ultrafast" + +#: GUI/widgets/settingsPage.py:1762 +msgid "veryfast" +msgstr "veryfast" + +#: GUI/widgets/settingsPage.py:1780 +msgid "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC. When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" +msgstr "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC. When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" + +#: GUI/widgets/settingsPage.py:1688 +msgid "Choose which lossy encoder to use. Auto chooses the best available." +msgstr "Elige qué codificador con pérdida usar. Automático elige el mejor disponible." + +#: GUI/widgets/settingsPage.py:1775 +msgid "Enable separate quality settings for podcasts and audiobooks. Music tracks are unaffected." +msgstr "Activa ajustes de calidad separados para podcasts y audiolibros. La música no se ve afectada." + +#: GUI/widgets/playlistBrowser.py:1667 +msgid "New Smart Playlist" +msgstr "Nueva lista inteligente" + +#: GUI/widgets/podcastBrowser.py:1441 +msgid "Add Podcast" +msgstr "Añadir podcast" + +#: GUI/widgets/podcastBrowser.py:1531 +msgid "Add Your First Podcast" +msgstr "Añadir tu primer podcast" + +#: GUI/widgets/podcastBrowser.py:451 +msgid "Add this episode to iPod" +msgstr "Añadir este episodio al iPod" + +#: GUI/widgets/podcastBrowser.py:469 GUI/widgets/podcastBrowser.py:449 GUI/widgets/podcastBrowser.py:2102 +msgid "Add to iPod" +msgstr "Añadir al iPod" + +#: GUI/widgets/podcastBrowser.py:1469 +msgid "Apply per-feed settings: remove listened/old episodes, fill empty slots with new episodes" +msgstr "Aplicar ajustes por feed: eliminar episodios escuchados/antiguos y llenar huecos con episodios nuevos" + +#: GUI/widgets/podcastBrowser.py:235 +msgid "Downloaded" +msgstr "Descargado" + +#: GUI/widgets/podcastBrowser.py:2247 +msgid "Downloaded: {count}" +msgstr "Descargados: {count}" + +#: GUI/widgets/podcastBrowser.py:260 +msgid "Episode" +msgstr "Episodio" + +#: GUI/widgets/podcastBrowser.py:2244 +msgid "Episodes: {count}" +msgstr "Episodios: {count}" + +#: GUI/widgets/podcastBrowser.py:516 GUI/widgets/podcastBrowser.py:596 GUI/widgets/podcastBrowser.py:525 +msgid "More" +msgstr "Más" + +#: GUI/widgets/podcastBrowser.py:1508 +msgid "No Podcast Subscriptions" +msgstr "No hay suscripciones a podcasts" + +#: GUI/widgets/podcastBrowser.py:586 +msgid "No description available." +msgstr "No hay descripción disponible." + +#: GUI/widgets/podcastBrowser.py:233 +msgid "On iPod" +msgstr "En iPod" + +#: GUI/widgets/podcastBrowser.py:2250 +msgid "On iPod: {count}" +msgstr "En iPod: {count}" + +#: GUI/widgets/podcastBrowser.py:2240 +msgid "RSS feed linked" +msgstr "Feed RSS vinculado" + +#: GUI/widgets/podcastBrowser.py:1451 +msgid "Refresh All" +msgstr "Actualizar todo" + +#: GUI/widgets/podcastBrowser.py:2024 +msgid "Refresh Feed" +msgstr "Actualizar feed" + +#: GUI/widgets/podcastBrowser.py:2109 +msgid "Remove Download" +msgstr "Eliminar descarga" + +#: GUI/widgets/podcastBrowser.py:504 GUI/widgets/podcastBrowser.py:480 GUI/widgets/podcastBrowser.py:2116 +msgid "Remove from iPod" +msgstr "Eliminar del iPod" + +#: GUI/widgets/podcastBrowser.py:484 +msgid "Remove this episode from iPod" +msgstr "Eliminar este episodio del iPod" + +#: GUI/widgets/podcastBrowser.py:1518 +msgid "" +"Search for podcasts or add an RSS feed to get started.\n" +"Episodes can be downloaded and synced to your iPod." +msgstr "" +"Busca podcasts o añade un feed RSS para empezar.\n" +"Los episodios se pueden descargar y sincronizar con tu iPod." + +#: GUI/widgets/podcastBrowser.py:1648 GUI/widgets/podcastBrowser.py:2209 +msgid "Select a podcast" +msgstr "Seleccionar un podcast" + +#: GUI/widgets/podcastBrowser.py:596 GUI/widgets/podcastBrowser.py:526 +msgid "Show less" +msgstr "Mostrar menos" + +#: GUI/widgets/podcastBrowser.py:1461 +msgid "Sync Podcasts" +msgstr "Sincronizar podcasts" + +#: GUI/widgets/podcastBrowser.py:2226 +msgid "Unknown Author" +msgstr "Autor desconocido" + +#: GUI/widgets/podcastBrowser.py:2026 +msgid "Unsubscribe" +msgstr "Cancelar suscripción" + +#: GUI/widgets/podcastBrowser.py:637 GUI/widgets/podcastBrowser.py:584 +msgid "Untitled Episode" +msgstr "Episodio sin título" + +#: GUI/widgets/podcastBrowser.py:2225 +msgid "Untitled Podcast" +msgstr "Podcast sin título" + +#: GUI/widgets/podcastBrowser.py:2238 +msgid "Updated {date}" +msgstr "Actualizado {date}" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Day" +msgstr "1 día" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Month" +msgstr "1 mes" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Week" +msgstr "1 semana" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "2 Months" +msgstr "2 meses" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "2 Weeks" +msgstr "2 semanas" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "3 Days" +msgstr "3 días" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "3 Months" +msgstr "3 meses" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "Adding podcast…" +msgstr "Añadiendo podcast…" + +#: GUI/widgets/podcastBrowser.py:2547 +msgid "All podcasts are up to date" +msgstr "Todos los podcasts están actualizados" + +#: GUI/widgets/podcastBrowser.py:2825 +msgid "All selected episodes are already on iPod" +msgstr "Todos los episodios seleccionados ya están en el iPod" + +#: GUI/widgets/podcastBrowser.py:2581 +msgid "Already subscribed" +msgstr "Ya suscrito" + +#: GUI/widgets/podcastSearchDialog.py:192 +msgid "Checking podcast results now." +msgstr "Comprobando resultados de podcasts." + +#: GUI/widgets/podcastBrowser.py:2371 +msgid "Checking subscribed feeds for new episodes." +msgstr "Comprobando episodios nuevos en feeds suscritos." + +#: GUI/widgets/podcastBrowser.py:2671 +msgid "Checking the feed for the latest episodes." +msgstr "Comprobando los episodios más recientes del feed." + +#: GUI/widgets/podcastBrowser.py:1775 +msgid "Clear method:" +msgstr "Método de limpieza:" + +#: GUI/widgets/podcastBrowser.py:1777 +msgid "Clear older than:" +msgstr "Eliminar más antiguos que:" + +#: GUI/widgets/podcastBrowser.py:1776 +msgid "Clear when listened:" +msgstr "Eliminar al escuchar:" + +#: GUI/widgets/podcastBrowser.py:2657 +msgid "Could not add podcast" +msgstr "No se pudo añadir el podcast" + +#: GUI/widgets/podcastBrowser.py:2931 +msgid "Episodes not found on iPod" +msgstr "Episodios no encontrados en el iPod" + +#: GUI/widgets/podcastBrowser.py:1773 +msgid "Episodes:" +msgstr "Episodios:" + +#: GUI/widgets/podcastBrowser.py:2584 +msgid "Fetching feed…" +msgstr "Obteniendo feed…" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "Fetching the feed and latest episodes." +msgstr "Obteniendo el feed y episodios recientes." + +#: GUI/widgets/podcastBrowser.py:1774 +msgid "Fill with:" +msgstr "Llenar con:" + +#: GUI/widgets/podcastSearchDialog.py:118 +msgid "Find podcasts" +msgstr "Buscar podcasts" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "Immediately" +msgstr "Inmediatamente" + +#: GUI/widgets/podcastBrowser.py:2330 +msgid "Loading episodes…" +msgstr "Cargando episodios…" + +#: GUI/widgets/podcastBrowser.py:1765 +msgid "Mark for Replacement" +msgstr "Marcar para reemplazo" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "Never" +msgstr "Nunca" + +#: GUI/widgets/podcastBrowser.py:1764 +msgid "Newest Episode" +msgstr "Episodio más reciente" + +#: GUI/widgets/podcastBrowser.py:1764 +msgid "Next Episode" +msgstr "Siguiente episodio" + +#: GUI/widgets/podcastBrowser.py:2281 +msgid "No episodes found" +msgstr "No se encontraron episodios" + +#: GUI/widgets/podcastBrowser.py:2815 +msgid "No episodes to sync" +msgstr "No hay episodios para sincronizar" + +#: GUI/widgets/podcastBrowser.py:2772 GUI/widgets/podcastBrowser.py:2899 +msgid "No iPod connected" +msgstr "No hay iPod conectado" + +#: GUI/widgets/podcastSearchDialog.py:213 +msgid "No podcasts found" +msgstr "No se encontraron podcasts" + +#: GUI/widgets/podcastBrowser.py:2366 +msgid "No subscriptions to refresh" +msgstr "No hay suscripciones para actualizar" + +#: GUI/widgets/podcastBrowser.py:2476 +msgid "No subscriptions to sync" +msgstr "No hay suscripciones para sincronizar" + +#: GUI/widgets/podcastBrowser.py:2446 +msgid "Podcasts could not refresh" +msgstr "No se pudieron actualizar los podcasts" + +#: GUI/widgets/podcastBrowser.py:2481 +msgid "Preparing podcast sync…" +msgstr "Preparando sincronización de podcasts…" + +#: GUI/widgets/podcastBrowser.py:2457 +msgid "Refresh failed" +msgstr "Error al actualizar" + +#: GUI/widgets/podcastBrowser.py:2481 +msgid "Refreshing feeds before building the sync plan." +msgstr "Actualizando feeds antes de crear el plan de sincronización." + +#: GUI/widgets/podcastBrowser.py:2480 +msgid "Refreshing feeds for sync…" +msgstr "Actualizando feeds para sincronizar…" + +#: GUI/widgets/podcastBrowser.py:2371 +msgid "Refreshing podcasts…" +msgstr "Actualizando podcasts…" + +#: GUI/widgets/podcastBrowser.py:2671 +msgid "Refreshing this podcast…" +msgstr "Actualizando este podcast…" + +#: GUI/widgets/podcastBrowser.py:1765 +msgid "Remove Immediately" +msgstr "Eliminar inmediatamente" + +#: GUI/widgets/podcastSearchDialog.py:118 +msgid "Search by show name, or paste an RSS feed below." +msgstr "Busca por nombre del programa o pega un feed RSS abajo." + +#: GUI/widgets/podcastSearchDialog.py:192 +msgid "Searching for podcasts…" +msgstr "Buscando podcasts…" + +#: GUI/widgets/podcastBrowser.py:2756 +msgid "Select episodes first" +msgstr "Selecciona episodios primero" + +#: GUI/widgets/podcastBrowser.py:2784 +msgid "Selected episodes are already on iPod" +msgstr "Los episodios seleccionados ya están en el iPod" + +#: GUI/widgets/podcastBrowser.py:2786 +msgid "Selected episodes cannot be added yet" +msgstr "Los episodios seleccionados aún no se pueden añadir" + +#: GUI/widgets/podcastBrowser.py:2448 +msgid "Some podcasts could not refresh" +msgstr "Algunos podcasts no se pudieron actualizar" + +#: GUI/widgets/podcastBrowser.py:2196 +msgid "Subscribed shows will appear here after their feeds refresh." +msgstr "Los programas suscritos aparecerán aquí tras actualizar sus feeds." + +#: GUI/widgets/podcastBrowser.py:2569 +msgid "Sync failed" +msgstr "Sincronización fallida" + +#: GUI/widgets/podcastBrowser.py:2471 GUI/widgets/podcastBrowser.py:2769 +msgid "This iPod does not support podcasts" +msgstr "Este iPod no admite podcasts" + +#: GUI/widgets/podcastBrowser.py:2281 +msgid "This podcast loaded, but its feed did not list any episodes." +msgstr "Este podcast se cargó, pero su feed no enumera episodios." + +#: GUI/widgets/podcastSearchDialog.py:213 +msgid "Try a different show name, or paste the RSS feed directly." +msgstr "Prueba otro nombre de programa o pega directamente el feed RSS." + +#: GUI/widgets/podcastBrowser.py:2196 +msgid "Waiting for episodes" +msgstr "Esperando episodios" + + +#: GUI/widgets/podcastBrowser.py:2895 GUI/widgets/podcastBrowser.py:2896 +msgid "Failed: {error}" +msgstr "Error: {error}" + +#: GUI/widgets/podcastBrowser.py:2593 GUI/widgets/podcastBrowser.py:2594 +msgid "Podcast sync: {summary}" +msgstr "Sincronización de podcasts: {summary}" + +#: GUI/widgets/podcastBrowser.py:2425 +msgid "Refreshed {count} feed" +msgstr "{count} feed actualizado" + +#: GUI/widgets/podcastBrowser.py:2427 +msgid "Refreshed {count} feeds" +msgstr "{count} feeds actualizados" + +#: GUI/widgets/podcastBrowser.py:2454 +msgid "Refreshed {count}; {failures} feed could not update" +msgstr "{count} actualizados; {failures} feed no se pudo actualizar" + +#: GUI/widgets/podcastBrowser.py:2459 +msgid "Refreshed {count}; {failures} feeds could not update" +msgstr "{count} actualizados; {failures} feeds no se pudieron actualizar" + +#: GUI/widgets/podcastBrowser.py:2745 GUI/widgets/podcastBrowser.py:2746 +msgid "Refreshed {title}" +msgstr "{title} actualizado" + +#: GUI/widgets/podcastBrowser.py:2374 +msgid "Refreshing {count} feeds…" +msgstr "Actualizando {count} feeds…" + +#: GUI/widgets/podcastBrowser.py:2372 +msgid "Refreshing {count} feed…" +msgstr "Actualizando {count} feed…" + +#: GUI/widgets/podcastBrowser.py:2715 GUI/widgets/podcastBrowser.py:2716 +msgid "Refreshing {title}…" +msgstr "Actualizando {title}…" + +#: GUI/widgets/podcastBrowser.py:2942 +msgid "Removed {count} download" +msgstr "{count} descarga eliminada" + +#: GUI/widgets/podcastBrowser.py:2944 +msgid "Removed {count} downloads" +msgstr "{count} descargas eliminadas" + +#: GUI/widgets/podcastBrowser.py:2881 +msgid "Sending {count} episode to sync…" +msgstr "Enviando {count} episodio para sincronizar…" + +#: GUI/widgets/podcastBrowser.py:2883 +msgid "Sending {count} episodes to sync…" +msgstr "Enviando {count} episodios para sincronizar…" + +#: GUI/widgets/podcastBrowser.py:3005 +msgid "Sending {count} removal to sync…" +msgstr "Enviando {count} eliminación para sincronizar…" + +#: GUI/widgets/podcastBrowser.py:3007 +msgid "Sending {count} removals to sync…" +msgstr "Enviando {count} eliminaciones para sincronizar…" + +#: GUI/widgets/podcastBrowser.py:2675 GUI/widgets/podcastBrowser.py:2676 +msgid "Subscribed to {title}" +msgstr "Suscrito a {title}" + +#: GUI/widgets/podcastBrowser.py:2705 GUI/widgets/podcastBrowser.py:2706 +msgid "Unsubscribed from {title}" +msgstr "Suscripción cancelada a {title}" + +#: GUI/widgets/podcastBrowser.py:2590 +msgid "{count} to add" +msgstr "{count} para añadir" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "{count} to remove" +msgstr "{count} para eliminar" + +#: GUI/widgets/playlistBrowser.py +msgid "Music" +msgstr "Música" + +#: GUI/widgets/playlistBrowser.py +msgid "Ringtones" +msgstr "Tonos" + +#: GUI/widgets/playlistBrowser.py +msgid "Rentals" +msgstr "Alquileres" diff --git a/locale/fr/LC_MESSAGES/iopenpod.mo b/locale/fr/LC_MESSAGES/iopenpod.mo new file mode 100644 index 0000000..c96e19a Binary files /dev/null and b/locale/fr/LC_MESSAGES/iopenpod.mo differ diff --git a/locale/fr/LC_MESSAGES/iopenpod.po b/locale/fr/LC_MESSAGES/iopenpod.po new file mode 100644 index 0000000..094a19e --- /dev/null +++ b/locale/fr/LC_MESSAGES/iopenpod.po @@ -0,0 +1,3025 @@ +#: GUI/widgets/photoBrowser.py:457 GUI/widgets/podcastBrowser.py:2330 +msgid "" +msgstr "" +"Project-Id-Version: iOpenPod\n" +"Report-Msgid-Bugs-To: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "" +"A crash report has been saved to:\n" +msgstr "" +"Un rapport de plantage a été enregistré dans :\n" + +#: GUI/widgets/settingsPage.py:885 +msgid "API Key" +msgstr "Clé API" + +#: GUI/widgets/settingsPage.py:976 +msgid "API Key and Secret required" +msgstr "Clé API et secret requis" + +#: GUI/widgets/settingsPage.py:892 +msgid "API Secret" +msgstr "Secret API" + +#: GUI/widgets/musicBrowser.py:336 +msgid "All Tracks" +msgstr "Tous les morceaux" + +#: GUI/widgets/settingsPage.py:1608 +msgid "About" +msgstr "À propos" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Accent Color" +msgstr "Couleur d’accent" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Advanced AAC" +msgstr "AAC avancé" + +#: GUI/widgets/photoBrowser.py:439 +msgid "Albums" +msgstr "Albums" + +msgid "Album" +msgstr "Album" + +msgid "" +"An unexpected error occurred:\n" +"\n" +msgstr "" +"Une erreur inattendue s’est produite :\n" +"\n" + +#: GUI/widgets/settingsPage.py:1608 +msgid "Appearance" +msgstr "Apparence" + +#: GUI/widgets/settingsPage.py:1546 +msgid "Apply a subtle display-only sharpening pass to album art in grid cards and track lists. This does not modify artwork written to your iPod." +msgstr "Apply a subtle display-only sharpening pass to album art in grid cards and track lists. This does not modify artwork written to your iPod." + +msgid "Artists" +msgstr "Artistes" + +msgid "Artist" +msgstr "Artiste" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Audio" +msgstr "Audio" + +msgid "Audiobooks" +msgstr "Livres audio" + +#: GUI/widgets/settingsPage.py:588 GUI/widgets/settingsPage.py:530 GUI/widgets/settingsPage.py:598 +msgid "Auto-detect" +msgstr "Détection automatique" + +msgid "Ask Each Time" +msgstr "Demander à chaque fois" + +#: GUI/widgets/settingsPage.py:1329 +msgid "Back" +msgstr "Retour" + +msgid "Backup Folder" +msgstr "Dossier de sauvegarde" + +#: GUI/widgets/settingsPage.py:2023 GUI/widgets/sidebar.py:906 +msgid "Backups" +msgstr "Sauvegardes" + +msgid "Backups and Rollback" +msgstr "Sauvegardes et restauration" + +#: GUI/widgets/settingsPage.py:1787 +msgid "Bandwidth Cutoff" +msgstr "Coupure de bande passante" + +msgid "Before Sync" +msgstr "Avant la synchronisation" + +#: GUI/widgets/settingsPage.py:1676 +msgid "Behavior" +msgstr "Comportement" + +msgid "Duration" +msgstr "Durée" + +#: GUI/widgets/settingsPage.py:1709 +msgid "Bitrate Mode" +msgstr "Mode de débit" + +#: GUI/widgets/settingsPage.py:1507 +msgid "Boost text and border contrast for accessibility. System follows your OS accessibility setting." +msgstr "Boost text and border contrast for accessibility. System follows your OS accessibility setting." + +msgid "Browse..." +msgstr "Parcourir..." + +#: GUI/widgets/settingsPage.py:306 GUI/widgets/settingsPage.py:403 GUI/widgets/settingsPage.py:537 +msgid "Browse…" +msgstr "Parcourir…" + +msgid "Cache Status" +msgstr "État du cache" + +msgid "Calculating…" +msgstr "Calcul…" + +#: GUI/app.py:2187 GUI/widgets/settingsPage.py:912 GUI/widgets/settingsPage.py:2959 +msgid "Cancel" +msgstr "Annuler" + +#: GUI/widgets/settingsPage.py:1114 +msgid "Canceled" +msgstr "Annulé" + +#: GUI/widgets/settingsPage.py:1554 GUI/widgets/settingsPage.py:2883 +msgid "Check" +msgstr "Vérifier" + +#: GUI/widgets/settingsPage.py:1554 +msgid "Check for a newer version of iOpenPod." +msgstr "Check for a newer version of iOpenPod." + +#: GUI/widgets/settingsPage.py:631 GUI/widgets/settingsPage.py:2877 +msgid "Checking…" +msgstr "Vérification…" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Choose the color scheme for the interface. System follows your OS preference." +msgstr "Choose the color scheme for the interface. System follows your OS preference." + +#: GUI/widgets/settingsPage.py:1488 +msgid "Choose the interface language. Changing it rebuilds the window." +msgstr "Choose the interface language. Changing it rebuilds the window." + +#: GUI/widgets/settingsPage.py:1134 +msgid "Clear Cache" +msgstr "Vider le cache" + +#: GUI/widgets/settingsPage.py:1175 +msgid "Clear Transcode Cache" +msgstr "Vider le cache de transcodage" + +#: GUI/widgets/sidebar.py:193 +msgid "Click to rename your iPod" +msgstr "Cliquez pour renommer votre iPod" + +#: GUI/widgets/settingsPage.py:1640 +msgid "Compute Sound Check" +msgstr "Calculer Sound Check" + +#: GUI/widgets/settingsPage.py:744 GUI/widgets/settingsPage.py:796 GUI/widgets/settingsPage.py:901 GUI/widgets/settingsPage.py:962 GUI/widgets/settingsPage.py:3211 +msgid "Connect" +msgstr "Connecter" + +#: GUI/widgets/settingsPage.py:781 GUI/widgets/settingsPage.py:942 +msgid "Connected as" +msgstr "Connecté en tant que" + +#: GUI/widgets/settingsPage.py:1746 +msgid "Convert WAV to ALAC" +msgstr "Convertir WAV en ALAC" + +#: GUI/widgets/settingsPage.py:1481 +msgid "Copy the current global values into this iPod's settings file." +msgstr "Copy the current global values into this iPod's settings file." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Customize the accent color used throughout the interface. Match iPod uses the body color of your connected iPod." +msgstr "Customize the accent color used throughout the interface. Match iPod uses the body color of your connected iPod." + +#: GUI/widgets/settingsPage.py:1176 +msgid "" +"Delete all cached transcoded files?\n" +"\n" +"They will be re-created on the next sync." +msgstr "" +"Supprimer tous les fichiers transcodés en cache ?\n" +"\n" +"Ils seront recréés à la prochaine synchronisation." + +#: GUI/widgets/settingsPage.py:1379 GUI/widgets/settingsPage.py:2055 +msgid "Device" +msgstr "Appareil" + +#: GUI/widgets/settingsPage.py:2055 +msgid "Device..." +msgstr "Appareil..." + +#: GUI/widgets/settingsPage.py:762 GUI/widgets/settingsPage.py:927 +msgid "Disconnect" +msgstr "Déconnecter" + +#: GUI/app.py:2589 GUI/widgets/settingsPage.py:636 GUI/widgets/settingsPage.py:3115 GUI/widgets/settingsPage.py:3122 +msgid "Download" +msgstr "Télécharger" + +#: GUI/widgets/settingsPage.py:671 GUI/widgets/settingsPage.py:672 +msgid "Downloading…" +msgstr "Téléchargement…" + +#: GUI/widgets/settingsPage.py:1893 +msgid "External Tools" +msgstr "Outils externes" + +#: GUI/widgets/settingsPage.py:1869 +msgid "FFmpeg" +msgstr "FFmpeg" + +#: GUI/widgets/settingsPage.py:1882 +msgid "FFmpeg Path Override" +msgstr "Remplacement du chemin FFmpeg" + +#: GUI/widgets/settingsPage.py:979 +msgid "Fetching token..." +msgstr "Récupération du jeton..." + +#: GUI/widgets/settingsPage.py:1653 +msgid "Fit Thumbnails" +msgstr "Adapter les miniatures" + +#: GUI/widgets/settingsPage.py:1515 +msgid "Font Size" +msgstr "Taille de police" + +#: GUI/widgets/settingsPage.py:1608 GUI/widgets/settingsPage.py:1928 +msgid "General" +msgstr "Général" + +msgid "Genres" +msgstr "Genres" + +#: GUI/widgets/settingsPage.py:862 +msgid "Get API keys ↗" +msgstr "Obtenir les clés API ↗" + +#: GUI/widgets/settingsPage.py:718 +msgid "Get token ↗" +msgstr "Obtenir le jeton ↗" + +#: GUI/widgets/settingsPage.py:1378 +msgid "Global" +msgstr "Global" + +#: GUI/widgets/sidebar.py:276 +msgid "Hours" +msgstr "Heures" + +#: GUI/widgets/settingsPage.py:1475 +msgid "Ignore this iPod's settings file and use the PC settings while this iPod is selected." +msgstr "Ignore this iPod's settings file and use the PC settings while this iPod is selected." + +#: GUI/widgets/settingsPage.py:1507 +msgid "Increased Contrast" +msgstr "Contraste renforcé" + +#: GUI/widgets/settingsPage.py:1819 +msgid "Intensity Stereo" +msgstr "Stéréo d’intensité" + +#: GUI/app.py:627 GUI/app.py:611 +msgid "Invalid iPod Folder" +msgstr "Dossier iPod non valide" + +#: GUI/widgets/settingsPage.py:1488 +msgid "Language" +msgstr "Langue" + +#: GUI/widgets/settingsPage.py:1918 +msgid "Last.fm" +msgstr "Last.fm" + +#: GUI/widgets/sidebar.py:935 +msgid "Library" +msgstr "Bibliothèque" + +msgid "LIBRARY" +msgstr "BIBLIOTHÈQUE" + +msgid "Library (Master)" +msgstr "Bibliothèque (maître)" + +#: GUI/widgets/settingsPage.py:1908 +msgid "ListenBrainz" +msgstr "ListenBrainz" + +#: GUI/widgets/settingsPage.py:2058 +msgid "Loading device settings..." +msgstr "Chargement des réglages de l’appareil..." + +#: GUI/app.py:482 +msgid "Loading iPod..." +msgstr "Chargement de l’iPod..." + +msgid "Log Folder" +msgstr "Dossier des journaux" + +#: GUI/widgets/settingsPage.py:1688 +msgid "Lossy Encoder" +msgstr "Encodeur avec pertes" + +#: GUI/widgets/settingsPage.py:1608 +msgid "Manage" +msgstr "Gérer" + +msgid "Maximum Backups" +msgstr "Nombre maximal de sauvegardes" + +#: GUI/widgets/settingsPage.py:1813 +msgid "Mid/Side Stereo" +msgstr "Stéréo mid/side" + +#: GUI/widgets/settingsPage.py:1769 +msgid "Mono for Spoken Word" +msgstr "Mono pour voix parlée" + +msgid "Movies" +msgstr "Films" + +#: GUI/widgets/settingsPage.py:1716 +msgid "Music Bitrate" +msgstr "Débit musique" + +#: GUI/widgets/settingsPage.py:1702 +msgid "Music Quality" +msgstr "Qualité musique" + +msgid "Music Videos" +msgstr "Clips vidéo" + +#: GUI/widgets/podcastBrowser.py:1767 GUI/widgets/sidebar.py:41 +msgid "No" +msgstr "Non" + +#: GUI/app.py:1418 GUI/app.py:1575 GUI/app.py:2087 GUI/widgets/sidebar.py:781 GUI/widgets/sidebar.py:189 GUI/widgets/sidebar.py:514 GUI/widgets/sidebar.py:437 GUI/widgets/sidebar.py:518 +msgid "No Device" +msgstr "Aucun appareil" + +#: GUI/app.py:441 +msgid "" +"No device is currently selected.\n" +"Choose an iPod to access your library and sync tools." +msgstr "" +"Aucun appareil n’est sélectionné.\n" +"Choisissez un iPod pour accéder à votre bibliothèque et aux outils de synchronisation." + +#: GUI/widgets/sidebar.py:611 GUI/widgets/sidebar.py:573 +msgid "None" +msgstr "Aucun" + +msgid "Name" +msgstr "Nom" + +#: GUI/widgets/sidebar.py:915 +msgid "Normalize Tags" +msgstr "Normaliser les tags" + +#: GUI/widgets/settingsPage.py:1780 +msgid "Normalize to 44.1 kHz" +msgstr "Normaliser à 44,1 kHz" + +#: GUI/widgets/settingsPage.py:664 +msgid "Not found" +msgstr "Introuvable" + +#: GUI/widgets/settingsPage.py:299 GUI/widgets/settingsPage.py:366 +msgid "Not set" +msgstr "Non défini" + +#: GUI/widgets/settingsPage.py:1561 +msgid "Open" +msgstr "Ouvrir" + +#: GUI/widgets/settingsPage.py:1561 +msgid "Open the GitHub issue tracker to report problems or request features." +msgstr "Open the GitHub issue tracker to report problems or request features." + +#: GUI/widgets/settingsPage.py:289 GUI/widgets/settingsPage.py:382 +msgid "Open ↗" +msgstr "Ouvrir ↗" + +#: GUI/widgets/settingsPage.py:129 +msgid "Overridden by device settings" +msgstr "Remplacé par les réglages de l’appareil" + +#: GUI/widgets/settingsPage.py:1619 +msgid "Parallel Workers" +msgstr "Tâches parallèles" + +#: GUI/widgets/settingsPage.py:1627 +msgid "Parallel Writes" +msgstr "Écritures parallèles" + +#: GUI/widgets/settingsPage.py:737 +msgid "Paste token here…" +msgstr "Coller le jeton ici…" + +#: GUI/widgets/settingsPage.py:1807 +msgid "Perceptual Noise Substitution (PNS)" +msgstr "Perceptual Noise Substitution (PNS)" + +#: GUI/widgets/settingsPage.py:1676 +msgid "Performance" +msgstr "Performances" + +msgid "Photos" +msgstr "Photos" + +#: GUI/widgets/settingsPage.py:1943 GUI/widgets/settingsPage.py:1961 GUI/widgets/settingsPage.py:1968 +msgid "Platform default" +msgstr "Valeur par défaut de la plateforme" + +#: GUI/widgets/playlistBrowser.py:1353 +msgid "Playlists" +msgstr "Listes de lecture" + +msgid "PODCAST PLAYLISTS" +msgstr "LISTES DE PODCASTS" + +msgid "Please report this issue on GitHub." +msgstr "Veuillez signaler ce problème sur GitHub." + +msgid "Podcasts" +msgstr "Podcasts" + +#: GUI/widgets/settingsPage.py:1735 +msgid "Prefer Lossy Encoding" +msgstr "Préférer l’encodage avec pertes" + +#: GUI/widgets/sidebar.py:197 GUI/widgets/sidebar.py:784 +msgid "Press Select to choose your iPod" +msgstr "Appuyez sur Sélectionner pour choisir votre iPod" + +#: GUI/widgets/sidebar.py:918 +msgid "Preview and apply iPod-friendly tag fixes across the whole library." +msgstr "Preview and apply iPod-friendly tag fixes across the whole library." + +#: GUI/widgets/settingsPage.py:1658 +msgid "Rating Conflict Strategy" +msgstr "Stratégie de conflit de notes" + +msgid "REGULAR PLAYLISTS" +msgstr "LISTES STANDARD" + +#: GUI/app.py:488 +msgid "Reading library and device settings." +msgstr "Lecture de la bibliothèque et des réglages de l’appareil." + +#: GUI/widgets/settingsPage.py:1741 +msgid "Reencode Existing Lossy Files" +msgstr "Réencoder les fichiers avec pertes existants" + +#: GUI/widgets/settingsPage.py:1561 +msgid "Report a Bug" +msgstr "Signaler un bug" + +#: GUI/widgets/sidebar.py:873 +msgid "Rescan" +msgstr "Actualiser" + +#: GUI/widgets/settingsPage.py:1481 +msgid "Reset" +msgstr "Réinitialiser" + +#: GUI/widgets/settingsPage.py:1481 +msgid "Reset Device Settings" +msgstr "Réinitialiser les réglages de l’appareil" + +#: GUI/widgets/settingsPage.py:553 +msgid "Reset to auto-detect" +msgstr "Réinitialiser sur détection automatique" + +msgid "Size" +msgstr "Taille" + +msgid "Sort" +msgstr "Trier" + +msgid "SMART PLAYLISTS" +msgstr "LISTES INTELLIGENTES" + +msgid "INTERNAL BROWSE CATEGORIES" +msgstr "CATÉGORIES INTERNES" + +#: GUI/widgets/settingsPage.py:419 +msgid "Reset to default" +msgstr "Réinitialiser par défaut" + +#: GUI/widgets/settingsPage.py:1647 +msgid "Rotate Tall Photos on Device" +msgstr "Faire pivoter les photos verticales sur l’appareil" + +#: GUI/widgets/settingsPage.py:1539 +msgid "Round album art corners in grid cards and track lists. This only changes how artwork is drawn in iOpenPod and does not modify anything written to your iPod." +msgstr "Round album art corners in grid cards and track lists. This only changes how artwork is drawn in iOpenPod and does not modify anything written to your iPod." + +#: GUI/widgets/settingsPage.py:1539 +msgid "Rounded Artwork" +msgstr "Pochettes arrondies" + +#: GUI/widgets/sidebar.py:217 +msgid "Safely eject the iPod from your system" +msgstr "Éjecter l’iPod en toute sécurité" + +#: GUI/widgets/sidebar.py:774 +msgid "Save failed" +msgstr "Échec de l’enregistrement" + +#: GUI/widgets/sidebar.py:767 +msgid "Saved" +msgstr "Enregistré" + +#: GUI/widgets/sidebar.py:761 +msgid "Saving…" +msgstr "Enregistrement…" + +#: GUI/widgets/settingsPage.py:1515 +msgid "Scale text size across the interface for accessibility." +msgstr "Scale text size across the interface for accessibility." + +#: GUI/widgets/sidebar.py:611 +msgid "Scheme 1" +msgstr "Schéma 1" + +#: GUI/widgets/sidebar.py:611 +msgid "Scheme 2" +msgstr "Schéma 2" + +#: GUI/widgets/settingsPage.py:1928 +msgid "Scrobbling" +msgstr "Scrobbling" + +#: GUI/widgets/sidebar.py:872 +msgid "Select" +msgstr "Choisir" + +#: GUI/app.py:451 +msgid "Select Device" +msgstr "Sélectionner un appareil" + +#: GUI/widgets/settingsPage.py:579 +msgid "Select File" +msgstr "Sélectionner un fichier" + +#: GUI/widgets/settingsPage.py:350 GUI/widgets/settingsPage.py:464 +msgid "Select Folder" +msgstr "Sélectionner un dossier" + +#: GUI/app.py:434 +msgid "Select an iPod to continue" +msgstr "Sélectionnez un iPod pour continuer" + +#: GUI/widgets/settingsPage.py:2060 +msgid "Select an iPod to edit device settings" +msgstr "Sélectionnez un iPod pour modifier les réglages de l’appareil" + +#: GUI/widgets/settingsPage.py:1335 GUI/widgets/sidebar.py:977 +msgid "Settings" +msgstr "Paramètres" + +msgid "Settings Folder" +msgstr "Dossier des paramètres" + +#: GUI/widgets/settingsPage.py:1546 +msgid "Sharpen Artwork" +msgstr "Accentuer les pochettes" + +#: GUI/widgets/settingsPage.py:1534 +msgid "Show album art thumbnails next to tracks in the list view." +msgstr "Show album art thumbnails next to tracks in the list view." + +#: GUI/widgets/settingsPage.py:1775 +msgid "Smart Quality by Content Type" +msgstr "Qualité intelligente par type de contenu" + +#: GUI/widgets/sidebar.py:275 +msgid "Songs" +msgstr "Morceaux" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Spoken Word" +msgstr "Voix parlée" + +#: GUI/widgets/settingsPage.py:1728 +msgid "Spoken Word Bitrate" +msgstr "Débit voix parlée" + +#: GUI/widgets/settingsPage.py:1893 +msgid "Status" +msgstr "État" + +#: GUI/widgets/settingsPage.py:1976 GUI/widgets/sidebar.py:401 +msgid "Storage" +msgstr "Stockage" + +msgid "Storage Paths" +msgstr "Chemins de stockage" + +#: GUI/widgets/settingsPage.py:1572 +msgid "Support iOpenPod" +msgstr "Soutenir iOpenPod" + +#: GUI/widgets/settingsPage.py:1676 +msgid "Sync" +msgstr "Synchronisation" + +#: GUI/widgets/sidebar.py:896 +msgid "Sync with PC" +msgstr "Synchroniser avec le PC" + +msgid "TV Shows" +msgstr "Séries TV" + +#: GUI/widgets/sidebar.py:284 +msgid "Technical Details" +msgstr "Détails techniques" + +#: GUI/widgets/settingsPage.py:1801 +msgid "Temporal Noise Shaping (TNS)" +msgstr "Modelage temporel du bruit (TNS)" + +#: GUI/app.py:612 +msgid "The selected folder could not be identified as an iPod." +msgstr "The selected folder could not be identified as an iPod." + +#: GUI/app.py:628 +msgid "" +"The selected folder does not appear to be a valid iPod root.\n" +"\n" +"Expected structure:\n" +" /iPod_Control/iTunes/\n" +"\n" +"Please select the root folder of your iPod." +msgstr "" +"The selected folder does not appear to be a valid iPod root.\n" +"\n" +"Expected structure:\n" +" /iPod_Control/iTunes/\n" +"\n" +"Please select the root folder of your iPod." + +#: GUI/widgets/settingsPage.py:1495 +msgid "Theme" +msgstr "Thème" + +#: GUI/widgets/settingsPage.py:1534 +msgid "Track List Artwork" +msgstr "Pochettes dans la liste des morceaux" + +#: GUI/widgets/trackListTitleBar.py:141 +msgid "Tracks" +msgstr "Morceaux" + +#: GUI/widgets/settingsPage.py:1976 +msgid "Transcode Cache" +msgstr "Cache de transcodage" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Transcoding" +msgstr "Transcodage" + +#: GUI/widgets/settingsPage.py:1475 +msgid "Use Global Settings" +msgstr "Utiliser les paramètres globaux" + +#: GUI/widgets/settingsPage.py:1722 +msgid "VBR Quality Level" +msgstr "Niveau de qualité VBR" + +#: GUI/widgets/settingsPage.py:1853 +msgid "Video" +msgstr "Vidéo" + +#: GUI/widgets/settingsPage.py:1762 +msgid "Video Encode Speed" +msgstr "Vitesse d’encodage vidéo" + +#: GUI/widgets/settingsPage.py:1751 +msgid "Video Quality (CRF)" +msgstr "Qualité vidéo (CRF)" + +msgid "Videos" +msgstr "Vidéos" + +#: GUI/widgets/settingsPage.py:1016 +msgid "Waiting for browser approval..." +msgstr "En attente de l’autorisation du navigateur..." + +#: GUI/widgets/settingsPage.py:1635 +msgid "Write Back to PC" +msgstr "Réécrire sur le PC" + +#: GUI/widgets/MBListView.py:168 GUI/widgets/MBListView.py:173 GUI/widgets/podcastBrowser.py:1767 GUI/widgets/sidebar.py:41 +msgid "Yes" +msgstr "Oui" + +#: GUI/widgets/settingsPage.py:1875 +msgid "fpcalc (Chromaprint)" +msgstr "fpcalc (Chromaprint)" + +#: GUI/widgets/settingsPage.py:1887 +msgid "fpcalc Path Override" +msgstr "fpcalc Path Override" + +msgid "iOpenPod Error" +msgstr "Erreur iOpenPod" + +#: GUI/widgets/settingsPage.py:1572 +msgid "iOpenPod is and always will be completely free and open source. If you like it and would like to support me, it is so very appreciated." +msgstr "iOpenPod is and always will be completely free and open source. If you like it and would like to support me, it is so very appreciated." + +#: GUI/app.py:661 +msgid "iPod Not Writable" +msgstr "iPod non inscriptible" + +#: GUI/widgets/settingsPage.py:1795 +msgid "libfdk_aac Afterburner" +msgstr "libfdk_aac Afterburner" + +#: GUI/widgets/sidebar.py:675 GUI/widgets/sidebar.py:681 +msgid "{free:.1f} GB free of {total:.1f} GB" +msgstr "{free:.1f} Go libres sur {total:.1f} Go" + +#: GUI/widgets/MBListView.py:2746 +msgid "(all columns shown)" +msgstr "(toutes les colonnes affichées)" + +msgid "{count:,} audiobook" +msgstr "{count:,} livre audio" + +msgid "{count:,} audiobooks" +msgstr "{count:,} livres audio" + +msgid "{count:,} episode" +msgstr "{count:,} épisode" + +msgid "{count:,} episodes" +msgstr "{count:,} épisodes" + +msgid "{count:,} song" +msgstr "{count:,} morceau" + +msgid "{count:,} songs" +msgstr "{count:,} morceaux" + +msgid "{count:,} track" +msgstr "{count:,} morceau" + +msgid "{count:,} tracks" +msgstr "{count:,} morceaux" + +msgid "{count:,} video" +msgstr "{count:,} vidéo" + +msgid "{count:,} videos" +msgstr "{count:,} vidéos" + +msgid "{shown:,} of {total:,} audiobook" +msgstr "{shown:,} of {total:,} audiobook" + +msgid "{shown:,} of {total:,} audiobooks" +msgstr "{shown:,} of {total:,} audiobooks" + +msgid "{shown:,} of {total:,} episode" +msgstr "{shown:,} sur {total:,} épisode" + +msgid "{shown:,} of {total:,} episodes" +msgstr "{shown:,} sur {total:,} épisodes" + +msgid "{shown:,} of {total:,} song" +msgstr "{shown:,} sur {total:,} morceau" + +msgid "{shown:,} of {total:,} songs" +msgstr "{shown:,} sur {total:,} morceaux" + +msgid "{shown:,} of {total:,} track" +msgstr "{shown:,} sur {total:,} morceau" + +msgid "{shown:,} of {total:,} tracks" +msgstr "{shown:,} sur {total:,} morceaux" + +msgid "{shown:,} of {total:,} video" +msgstr "{shown:,} sur {total:,} vidéo" + +msgid "{shown:,} of {total:,} videos" +msgstr "{shown:,} sur {total:,} vidéos" + +msgid "{gb:.2f} GB used of {max_gb:.0f} GB · {count:,} file" +msgstr "{gb:.2f} Go utilisés sur {max_gb:.0f} Go · {count:,} fichier" + +msgid "{gb:.2f} GB used of {max_gb:.0f} GB · {count:,} files" +msgstr "{gb:.2f} Go utilisés sur {max_gb:.0f} Go · {count:,} fichiers" + +msgid "{gb:.2f} GB · {count:,} file" +msgstr "{gb:.2f} Go · {count:,} fichier" + +msgid "{gb:.2f} GB · {count:,} files" +msgstr "{gb:.2f} Go · {count:,} fichiers" + +#: GUI/widgets/MBListView.py:2646 +msgid "Add Column" +msgstr "Ajouter une colonne" + +msgid "Album Artist" +msgstr "Artiste de l’album" + +msgid "Album ID" +msgstr "ID album" + +msgid "Art Count" +msgstr "Nombre de pochettes" + +msgid "Art Formats:" +msgstr "Formats de pochette :" + +msgid "Artist Ref" +msgstr "Réf. artiste" + +msgid "Artwork" +msgstr "Pochette" + +msgid "Artwork Ref" +msgstr "Réf. pochette" + +msgid "Audio:" +msgstr "Audio :" + +msgid "Audio Quality" +msgstr "Qualité audio" + +msgid "Bitrate" +msgstr "Débit" + +msgid "BPM" +msgstr "BPM" + +msgid "Bus/Format:" +msgstr "Bus/Format:" + +#: GUI/widgets/settingsPage.py:1943 +msgid "Cache Location" +msgstr "Emplacement du cache" + +#: GUI/widgets/sidebar.py:392 +msgid "Capabilities" +msgstr "Capacités" + +msgid "Category" +msgstr "Catégorie" + +msgid "Chapter Img:" +msgstr "Chapter Img:" + +msgid "Chapter Titles" +msgstr "Titres des chapitres" + +msgid "Chapters" +msgstr "Chapitres" + +msgid "Cleared — {count:,} file removed" +msgstr "Cache vidé — {count:,} fichier supprimé" + +msgid "Cleared — {count:,} files removed" +msgstr "Cache vidé — {count:,} fichiers supprimés" + +msgid "Comment" +msgstr "Commentaire" + +msgid "Compilation" +msgstr "Compilation" + +msgid "Composer" +msgstr "Compositeur" + +msgid "Composer ID" +msgstr "Composer ID" + +msgid "Conflicts:" +msgstr "Conflits :" + +#: GUI/widgets/musicBrowser.py:471 +msgid "Convert to a single chaptered track" +msgstr "Convertir en un seul morceau chapitré" + +msgid "Core Metadata" +msgstr "Métadonnées principales" + +#: GUI/widgets/musicBrowser.py:560 +msgid "" +"Could not prepare artwork:\n" +"\n" +"{error}" +msgstr "" +"Could not prepare artwork:\n" +"\n" +"{error}" + +#: GUI/widgets/musicBrowser.py:582 +msgid "" +"Could not stage artwork update:\n" +"\n" +"{error}" +msgstr "" +"Could not stage artwork update:\n" +"\n" +"{error}" + +#: GUI/widgets/settingsPage.py:1961 +msgid "Custom directory to store iOpenPod settings. Useful for portable setups or backups." +msgstr "Custom directory to store iOpenPod settings. Useful for portable setups or backups." + +msgid "Date Added" +msgstr "Date d’ajout" + +msgid "Date Modified" +msgstr "Date de modification" + +msgid "Dates" +msgstr "Dates" + +msgid "Description" +msgstr "Description" + +msgid "Device DB:" +msgstr "Device DB:" + +msgid "Disc #" +msgstr "Disque n°" + +msgid "Disc Total" +msgstr "Total disques" + +msgid "Disk Size:" +msgstr "Taille du disque :" + +msgid "Encoder" +msgstr "Encodeur" + +msgid "Enclosure URL" +msgstr "Enclosure URL" + +msgid "Episode #" +msgstr "Épisode n°" + +msgid "Episode ID" +msgstr "ID épisode" + +msgid "Equalizer" +msgstr "Égaliseur" + +#: GUI/widgets/settingsPage.py:1194 +msgid "Error clearing cache: {error}" +msgstr "Error clearing cache: {error}" + +msgid "File Format" +msgstr "Format de fichier" + +msgid "Flags" +msgstr "Drapeaux" + +msgid "Free Space:" +msgstr "Espace libre :" + +msgid "Gapless" +msgstr "Gapless" + +msgid "Gapless Album" +msgstr "Gapless Album" + +msgid "Gapless Payload" +msgstr "Gapless Payload" + +msgid "Grouping" +msgstr "Groupement" + +msgid "Has Lyrics" +msgstr "Avec paroles" + +msgid "Hash Scheme:" +msgstr "Hash Scheme:" + +#: GUI/widgets/MBListView.py:2628 +msgid "Hide \"{column}\"" +msgstr "Masquer « {column} »" + +#: GUI/widgets/sidebar.py:364 +msgid "Identity" +msgstr "Identité" + +msgid "Identifiers" +msgstr "Identifiants" + +msgid "Keywords" +msgstr "Mots-clés" + +msgid "Largest" +msgstr "Plus grand" + +msgid "Last Played" +msgstr "Dernière lecture" + +msgid "Last Skipped" +msgstr "Dernier saut" + +#: GUI/widgets/settingsPage.py:1976 +msgid "Locations" +msgstr "Emplacements" + +msgid "Locale" +msgstr "Paramètres régionaux" + +msgid "Location" +msgstr "Emplacement" + +#: GUI/widgets/settingsPage.py:1968 +msgid "Log Location" +msgstr "Emplacement des journaux" + +msgid "Lyrics" +msgstr "Paroles" + +#: GUI/widgets/settingsPage.py:1950 +msgid "Max Cache Size" +msgstr "Taille max. du cache" + +msgid "Max File:" +msgstr "Max File:" + +msgid "Max Transfer:" +msgstr "Max Transfer:" + +msgid "Media Type" +msgstr "Type de média" + +#: GUI/widgets/trackListTitleBar.py:146 +msgid "Minimize" +msgstr "Réduire" + +msgid "Model #:" +msgstr "Model #:" + +msgid "Most Albums" +msgstr "Le plus d’albums" + +msgid "Most Artists" +msgstr "Le plus d’artistes" + +msgid "Most Plays" +msgstr "Le plus de lectures" + +msgid "Most Skipped" +msgstr "Le plus ignorés" + +msgid "Most Tracks" +msgstr "Le plus de morceaux" + +msgid "Network" +msgstr "Réseau" + +#: GUI/widgets/settingsPage.py:1950 +msgid "Oldest cached files are automatically removed (LRU) to stay within this limit. Set to Unlimited if storage is not a concern." +msgstr "Oldest cached files are automatically removed (LRU) to stay within this limit. Set to Unlimited if storage is not a concern." + +#: GUI/widgets/MBListView.py:2736 +msgid "Other" +msgstr "Autre" + +msgid "Photo Formats:" +msgstr "Formats photo :" + +msgid "Playback && Stats" +msgstr "Lecture et statistiques" + +msgid "Played" +msgstr "Lu" + +msgid "Plays" +msgstr "Lectures" + +msgid "Plays (iPod)" +msgstr "Lectures (iPod)" + +msgid "Podcast" +msgstr "Podcast" + +msgid "Podcasts:" +msgstr "Podcasts :" + +msgid "Post-gap" +msgstr "Post-gap" + +msgid "Pre-gap" +msgstr "Pré-gap" + +msgid "Product:" +msgstr "Product:" + +#: GUI/widgets/MBListView.py:3312 +msgid "Rating" +msgstr "Note" + +msgid "Release Date" +msgstr "Date de sortie" + +msgid "Remember Pos." +msgstr "Mémoriser la position" + +#: GUI/widgets/MBListView.py:2640 +msgid "Resize All Columns to Fit" +msgstr "Ajuster toutes les colonnes" + +#: GUI/widgets/MBListView.py:2635 +msgid "Resize Column to Fit" +msgstr "Ajuster la colonne" + +msgid "RSS URL" +msgstr "URL RSS" + +msgid "Sample Count" +msgstr "Nombre d’échantillons" + +msgid "Sample Rate" +msgstr "Fréquence d’échantillonnage" + +#: GUI/widgets/gridHeaderBar.py:77 +msgid "Search…" +msgstr "Rechercher…" + +msgid "Season" +msgstr "Saison" + +msgid "Select an Album" +msgstr "Sélectionner un album" + +msgid "Select an Artist" +msgstr "Sélectionner un artiste" + +msgid "Select a Genre" +msgstr "Sélectionner un genre" + +msgid "Serial:" +msgstr "Serial:" + +#: GUI/widgets/settingsPage.py:1961 +msgid "Settings Location" +msgstr "Emplacement des paramètres" + +msgid "Shadow DB:" +msgstr "Shadow DB:" + +msgid "Show" +msgstr "Émission" + +msgid "Skip Shuffle" +msgstr "Ignorer en lecture aléatoire" + +msgid "Skips" +msgstr "Sauts" + +#: GUI/widgets/gridHeaderBar.py:153 +msgid "Sort: {label} ▾" +msgstr "Trier : {label} ▾" + +msgid "Sort Album" +msgstr "Tri album" + +msgid "Sort Album Artist" +msgstr "Tri artiste album" + +msgid "Sort Artist" +msgstr "Tri artiste" + +msgid "Sort Composer" +msgstr "Tri compositeur" + +msgid "Sort Overrides" +msgstr "Remplacements de tri" + +msgid "Sort Show" +msgstr "Tri émission" + +msgid "Sort Title" +msgstr "Tri titre" + +msgid "Sparse Art:" +msgstr "Sparse Art:" + +msgid "Start Time" +msgstr "Heure de début" + +msgid "Stop Time" +msgstr "Heure de fin" + +msgid "Subtitle" +msgstr "Sous-titre" + +msgid "Time" +msgstr "Durée" + +msgid "Title" +msgstr "Titre" + +msgid "Track #" +msgstr "Piste n°" + +msgid "Track ID" +msgstr "ID morceau" + +msgid "Track Keywords" +msgstr "Mots-clés du morceau" + +msgid "Track Total" +msgstr "Total morceaux" + +#: GUI/widgets/settingsPage.py:1167 +msgid "Unavailable ({error})" +msgstr "Indisponible ({error})" + +#: GUI/widgets/musicBrowser.py:488 GUI/widgets/musicBrowser.py:559 GUI/widgets/musicBrowser.py:581 +msgid "Unify Artwork" +msgstr "Unifier les pochettes" + +msgid "Updater ID:" +msgstr "Updater ID:" + +#: GUI/widgets/sidebar.py:374 +msgid "USB / SCSI" +msgstr "USB / SCSI" + +msgid "USB Parent:" +msgstr "USB Parent:" + +msgid "USB PID:" +msgstr "USB PID:" + +msgid "USB Serial:" +msgstr "USB Serial:" + +msgid "USB VID:" +msgstr "USB VID:" + +msgid "Voice Memos:" +msgstr "Voice Memos:" + +msgid "Volume Adj." +msgstr "Ajustement du volume" + +#: GUI/widgets/settingsPage.py:1968 +msgid "Where iOpenPod writes log files and crash reports. Takes effect on next launch." +msgstr "Emplacement où iOpenPod écrit les journaux et rapports de plantage. Prend effet au prochain lancement." + +#: GUI/widgets/settingsPage.py:1943 +msgid "Where transcoded files are cached to avoid re-encoding on future syncs." +msgstr "Emplacement de cache des fichiers transcodés afin d’éviter un réencodage lors des synchronisations futures." + +msgid "Year" +msgstr "Année" + +#: GUI/widgets/sidebar.py:546 +msgid "" +"{value}\n" +"Source: {source}" +msgstr "" +"{value}\n" +"Source: {source}" + +#: GUI/widgets/MBListView.py:3322 GUI/widgets/MBListView.py:3360 +msgid "(mixed selection)" +msgstr "(sélection mixte)" + +#: GUI/widgets/MBListView.py:3102 +msgid "Add to Playlist" +msgstr "Ajouter à la liste de lecture" + +#: GUI/widgets/MBListView.py:3304 +msgid "Checked" +msgstr "Coché" + +#: GUI/widgets/MBListView.py:3351 +msgid "Content Advisory" +msgstr "Avertissement de contenu" + +#: GUI/widgets/MBListView.py:2364 +msgid "Content Advisory: Clean" +msgstr "Avertissement : version censurée" + +#: GUI/widgets/MBListView.py:2356 +msgid "Content Advisory: Explicit" +msgstr "Avertissement : explicite" + +#: GUI/widgets/MBListView.py:3209 +msgid "Convert to Podcast" +msgstr "Convertir en podcast" + +#: GUI/widgets/MBListView.py:3191 +msgid "Copy as File(s)" +msgstr "Copier comme fichier(s)" + +#: GUI/widgets/MBListView.py:3188 +msgid "Copy as Text" +msgstr "Copier comme texte" + +#: GUI/widgets/settingsPage.py:2959 +msgid "Downloading update…" +msgstr "Téléchargement de la mise à jour…" + +#: GUI/widgets/MBListView.py:2908 +msgid "Edit ({count})" +msgstr "Modifier ({count})" + +#: GUI/widgets/MBListView.py:153 GUI/widgets/MBListView.py:3367 +msgid "Explicit" +msgstr "Explicite" + +#: GUI/widgets/settingsPage.py:3060 +msgid "ffprobe missing" +msgstr "ffprobe missing" + +#: GUI/widgets/settingsPage.py:2961 +msgid "iOpenPod Update" +msgstr "iOpenPod Update" + +#: GUI/widgets/settingsPage.py:3214 +msgid "Invalid token" +msgstr "Jeton non valide" + +#: GUI/widgets/MBListView.py:3166 +msgid "Move Down" +msgstr "Descendre" + +#: GUI/widgets/MBListView.py:3162 +msgid "Move Up" +msgstr "Monter" + +#: GUI/widgets/MBListView.py:3106 GUI/widgets/playlistBrowser.py:1331 GUI/widgets/playlistBrowser.py:1674 +msgid "New Playlist" +msgstr "Nouvelle liste de lecture" + +#: GUI/widgets/settingsPage.py:2948 +msgid "No Binary Available" +msgstr "No Binary Available" + +#: GUI/widgets/settingsPage.py:2949 +msgid "" +"No pre-built binary was found for your platform.\n" +"\n" +"Visit {release_page} to download manually." +msgstr "" +"No pre-built binary was found for your platform.\n" +"\n" +"Visit {release_page} to download manually." + +#: GUI/widgets/MBListView.py:3328 +msgid "No Rating" +msgstr "Aucune note" + +#: GUI/widgets/MBListView.py:3366 +msgid "None (Unset)" +msgstr "Aucun (non défini)" + +msgid "Part of a compilation album" +msgstr "Fait partie d’une compilation" + +msgid "Preparing {count} file…" +msgstr "Préparation de {count} fichier…" + +msgid "Preparing {count} files…" +msgstr "Préparation de {count} fichiers…" + +msgid "Remove {count} Track from iPod" +msgstr "Retirer {count} morceau de l’iPod" + +msgid "Remove {count} Tracks from iPod" +msgstr "Retirer {count} morceaux de l’iPod" + +msgid "Remove {count} Track from Playlist" +msgstr "Retirer {count} morceau de la liste" + +msgid "Remove {count} Tracks from Playlist" +msgstr "Retirer {count} morceaux de la liste" + +#: GUI/widgets/MBListView.py:2752 +msgid "Reset Columns" +msgstr "Réinitialiser les colonnes" + +msgid "Skip When Shuffling" +msgstr "Ignorer en lecture aléatoire" + +msgid "Skip this track in shuffle mode" +msgstr "Ignorer ce morceau en lecture aléatoire" + +#: GUI/widgets/MBListView.py:3074 +msgid "Split chapters into individual tracks" +msgstr "Séparer les chapitres en morceaux" + +#: GUI/widgets/MBListView.py:773 +msgid "Starting drag…" +msgstr "Début du glisser-déposer…" + +#: GUI/widgets/MBListView.py:3116 +msgid "Untitled" +msgstr "Sans titre" + +#: GUI/widgets/settingsPage.py:2892 +msgid "Up to Date" +msgstr "À jour" + +#: GUI/widgets/settingsPage.py:2886 +msgid "Update Check Failed" +msgstr "Échec de la recherche de mise à jour" + +#: GUI/widgets/settingsPage.py:3180 +msgid "Validating…" +msgstr "Validation…" + +#: GUI/widgets/MBListView.py:3384 +msgid "Volume Adjustment" +msgstr "Ajustement du volume" + +#: GUI/widgets/settingsPage.py:2893 +msgid "You are running the latest version (v{version})." +msgstr "You are running the latest version (v{version})." + +#: GUI/widgets/MBListView.py:155 GUI/widgets/MBListView.py:3368 +msgid "Clean" +msgstr "Censuré" + +#: GUI/widgets/sidebar.py:383 +msgid "Database" +msgstr "Base de données" + +#: GUI/widgets/musicBrowser.py:463 GUI/widgets/playlistBrowser.py:419 +msgid "Edit" +msgstr "Modifier" + +#: GUI/widgets/trackListTitleBar.py:150 +msgid "Maximize" +msgstr "Agrandir" + +#: GUI/widgets/MBListView.py:215 +msgid "{count} chapter" +msgstr "{count} chapitre" + +#: GUI/widgets/MBListView.py:216 +msgid "{count} chapters" +msgstr "{count} chapitres" + +#: GUI/app.py:2312 +msgid "" +"\n" +"\n" +"If you discard, the copied files will be cleaned up automatically the next time you sync." +msgstr "" +"\n" +"\n" +"If you discard, the copied files will be cleaned up automatically the next time you sync." + +#: app_core/jobs.py:70 +msgid " and " +msgstr " and " + +#: GUI/app.py:1439 GUI/app.py:1433 +msgid "Album Conversion" +msgstr "Conversion d’album" + +#: GUI/app.py:1594 +msgid "Chapter Split" +msgstr "Découpage des chapitres" + +#: GUI/app.py:1440 +msgid "Choose an album with at least two tracks." +msgstr "Choisissez un album avec au moins deux morceaux." + +#: GUI/app.py:781 +msgid "Could Not Load iPod" +msgstr "Impossible de charger l’iPod" + +#: GUI/app.py:1379 +msgid "" +"Could not download sync tools:\n" +"\n" +"{error}" +msgstr "" +"Could not download sync tools:\n" +"\n" +"{error}" + +#: GUI/app.py:1102 +msgid "" +"Could not save quick changes to iPod:\n" +"{error}\n" +"\n" +"iOpenPod is reloading the device view from the iPod." +msgstr "" +"Could not save quick changes to iPod:\n" +"{error}\n" +"\n" +"iOpenPod is reloading the device view from the iPod." + +#: GUI/app.py:955 app_core/jobs.py:1940 app_core/jobs.py:1989 app_core/jobs.py:2265 +msgid "Database write failed." +msgstr "Échec de l’écriture de la base de données." + +#: GUI/app.py:958 +msgid "Device name updated successfully" +msgstr "Nom de l’appareil mis à jour" + +#: GUI/app.py:2317 +msgid "Discard" +msgstr "Ignorer" + +#: GUI/app.py:1378 +msgid "Download Failed" +msgstr "Échec du téléchargement" + +#: GUI/app.py:2625 GUI/widgets/podcastBrowser.py:237 +msgid "Downloading" +msgstr "Téléchargement" + +#: GUI/app.py:2638 +msgid "Downloading Tools…" +msgstr "Téléchargement des outils…" + +#: GUI/app.py:1075 +msgid "Eject Failed" +msgstr "Échec de l’éjection" + +#: app_core/jobs.py:84 +msgid "" +"FFmpeg and ffprobe are required for transcoding and media probing.\n" +"Install from: https://ffmpeg.org" +msgstr "" +"FFmpeg and ffprobe are required for transcoding and media probing.\n" +"Install from: https://ffmpeg.org" + +#: GUI/app.py:1076 +msgid "" +"Failed to eject the iPod:\n" +"{error}" +msgstr "" +"Failed to eject the iPod:\n" +"{error}" + +#: GUI/app.py:966 +msgid "" +"Failed to rename iPod:\n" +"{error}" +msgstr "" +"Failed to rename iPod:\n" +"{error}" + +#: GUI/app.py:1423 GUI/app.py:1582 GUI/app.py:1184 +msgid "Library Loading" +msgstr "Chargement de la bibliothèque" + +#: GUI/app.py:791 +msgid "Load an iPod library before running tag normalization." +msgstr "Load an iPod library before running tag normalization." + +#: GUI/app.py:2525 +msgid "Missing Tools" +msgstr "Outils manquants" + +#: app_core/jobs.py:1922 app_core/jobs.py:1971 +msgid "No iPod connected." +msgstr "Aucun iPod connecté." + +#: app_core/jobs.py:1926 app_core/jobs.py:1975 +msgid "No iPod database loaded." +msgstr "Aucune base de données iPod chargée." + +#: GUI/app.py:2087 +msgid "No iPod device selected." +msgstr "Aucun appareil iPod sélectionné." + +#: GUI/app.py:824 +msgid "No iPod-specific metadata fixes were found for this library." +msgstr "Aucune correction de métadonnées propre à l’iPod n’a été trouvée pour cette bibliothèque." + +#: GUI/app.py:800 +msgid "No tracks were found in this iPod library." +msgstr "Aucun morceau trouvé dans cette bibliothèque iPod." + +#: GUI/app.py:790 GUI/app.py:799 GUI/app.py:823 +msgid "Normalize iPod Tags" +msgstr "Normaliser les tags iPod" + +#: GUI/app.py:2184 +msgid "Not Enough Space" +msgstr "Espace insuffisant" + +#: GUI/app.py:2575 +msgid "Not Now" +msgstr "Pas maintenant" + +#: GUI/app.py:2603 +msgid "OK" +msgstr "OK" + +#: GUI/app.py:1418 GUI/app.py:1575 +msgid "Please select an iPod device first." +msgstr "Veuillez d’abord sélectionner un iPod." + +#: GUI/app.py:1412 +msgid "Please wait for the current sync to finish before converting an album." +msgstr "Veuillez attendre la fin de la synchronisation avant de convertir un album." + +#: GUI/app.py:1030 +msgid "Please wait for the current sync to finish before ejecting." +msgstr "Veuillez attendre la fin de la synchronisation avant d’éjecter." + +#: GUI/app.py:1569 +msgid "Please wait for the current sync to finish before splitting chapters." +msgstr "Veuillez attendre la fin de la synchronisation avant de séparer les chapitres." + +#: GUI/app.py:1423 GUI/app.py:1583 GUI/app.py:1185 +msgid "Please wait for the iPod library to finish loading." +msgstr "Veuillez attendre la fin du chargement de la bibliothèque iPod." + +#: GUI/app.py:2644 +msgid "Preparing download…" +msgstr "Préparation du téléchargement…" + +#: GUI/app.py:1174 +msgid "Quick Changes Still Saving" +msgstr "Les changements rapides sont encore en cours d’enregistrement" + +#: GUI/app.py:2421 +msgid "Reading dropped files..." +msgstr "Lecture des fichiers déposés..." + +#: GUI/app.py:965 +msgid "Rename Failed" +msgstr "Échec du renommage" + +#: GUI/app.py:1101 +msgid "Save Failed" +msgstr "Échec de l’enregistrement" + +#: GUI/app.py:979 +msgid "Save In Progress" +msgstr "Enregistrement en cours" + +#: GUI/app.py:2316 +msgid "Save Partial Database" +msgstr "Enregistrer une base partielle" + +#: GUI/app.py:2301 +msgid "Save Partial Sync?" +msgstr "Enregistrer la synchronisation partielle ?" + +#: GUI/app.py:1008 +msgid "Still Reading iPod" +msgstr "Lecture de l’iPod en cours" + +#: GUI/app.py:2280 +msgid "Sync Error" +msgstr "Erreur de synchronisation" + +#: GUI/app.py:2204 +msgid "Sync Failed" +msgstr "Échec de la synchronisation" + +#: GUI/app.py:1029 +msgid "Sync In Progress" +msgstr "Synchronisation en cours" + +#: GUI/app.py:1411 GUI/app.py:1568 +msgid "Sync Running" +msgstr "Synchronisation active" + +#: GUI/app.py:2189 +msgid "Sync Until Full" +msgstr "Synchroniser jusqu’à remplissage" + +#: GUI/app.py:167 +msgid "Sync failed before making changes." +msgstr "La synchronisation a échoué avant toute modification." + +#: GUI/app.py:2185 +msgid "The selected sync is larger than the iPod's free space." +msgstr "La synchronisation sélectionnée dépasse l’espace libre de l’iPod." + +#: GUI/app.py:1393 +msgid "This iPod does not support podcasts." +msgstr "Cet iPod ne prend pas en charge les podcasts." + +#: GUI/app.py:2166 +msgid "" +"This sync is estimated to need more space than is available on the iPod.\n" +"\n" +"Available: {available}\n" +"Estimated needed: {required}\n" +"Estimated shortfall: {shortage}\n" +"\n" +"Sync Until Full will copy files in order until the next file would leave less than {reserve} free, then save the database with the items that actually synced." +msgstr "" +"This sync is estimated to need more space than is available on the iPod.\n" +"\n" +"Available: {available}\n" +"Estimated needed: {required}\n" +"Estimated shortfall: {shortage}\n" +"\n" +"Sync Until Full will copy files in order until the next file would leave less than {reserve} free, then save the database with the items that actually synced." + +#: GUI/app.py:746 GUI/app.py:748 +msgid "Unknown iPod" +msgstr "iPod inconnu" + +#: GUI/app.py:1392 +msgid "Unsupported iPod" +msgstr "iPod non pris en charge" + +#: GUI/app.py:2309 +msgid "Would you like to save these tracks to your iPod's database?" +msgstr "Voulez-vous enregistrer ces morceaux dans la base de données de l’iPod ?" + +#: app_core/jobs.py:89 +msgid "" +"You can also set custom paths in\n" +"Settings -> External Tools." +msgstr "" +"Vous pouvez aussi définir des chemins personnalisés dans\n" +"Paramètres -> Outils externes." + +#: app_core/jobs.py:77 +msgid "" +"fpcalc is required for sync.\n" +"Install from: https://acoustid.org/chromaprint" +msgstr "" +"fpcalc is required for sync.\n" +"Install from: https://acoustid.org/chromaprint" + +#: GUI/app.py:2555 +msgid "" +"iOpenPod can download these automatically (~80 MB).\n" +"Download now?" +msgstr "" +"iOpenPod can download these automatically (~80 MB).\n" +"Download now?" + +#: GUI/app.py:186 +msgid "" +"iOpenPod could not load this iPod library.\n" +"\n" +"{error}" +msgstr "" +"iOpenPod could not load this iPod library.\n" +"\n" +"{error}" + +#: GUI/app.py:190 +msgid "" +"iOpenPod could not read this iPod cleanly.\n" +"\n" +"Mount path: {mount}\n" +"System error: {error}\n" +"\n" +"On Linux, this usually means the iPod mount is not accessible, the FAT filesystem is dirty, or the current user does not have permission to the mount.\n" +"\n" +"Try reconnecting the iPod. If it still fails, try remounting it read-write:\n" +" sudo mount -o remount,rw {quoted_mount}\n" +"\n" +"If the filesystem is dirty, unmount it before repairing it:\n" +" sudo umount {quoted_mount}\n" +" sudo fsck.vfat -a /dev/sdXN\n" +"\n" +"Replace /dev/sdXN with the iPod partition. Do not run fsck while the iPod is mounted." +msgstr "" +"iOpenPod could not read this iPod cleanly.\n" +"\n" +"Mount path: {mount}\n" +"System error: {error}\n" +"\n" +"On Linux, this usually means the iPod mount is not accessible, the FAT filesystem is dirty, or the current user does not have permission to the mount.\n" +"\n" +"Try reconnecting the iPod. If it still fails, try remounting it read-write:\n" +" sudo mount -o remount,rw {quoted_mount}\n" +"\n" +"If the filesystem is dirty, unmount it before repairing it:\n" +" sudo umount {quoted_mount}\n" +" sudo fsck.vfat -a /dev/sdXN\n" +"\n" +"Replace /dev/sdXN with the iPod partition. Do not run fsck while the iPod is mounted." + +#: GUI/app.py:1009 +msgid "iOpenPod is still finishing background reads from the iPod. Try ejecting again in a moment." +msgstr "iOpenPod is still finishing background reads from the iPod. Try ejecting again in a moment." + +#: GUI/app.py:1175 +msgid "iOpenPod is still saving pending quick changes. Please wait for {label} to finish before starting a full sync." +msgstr "iOpenPod is still saving pending quick changes. Please wait for {label} to finish before starting a full sync." + +#: GUI/app.py:980 +msgid "iOpenPod is still saving {label} to the iPod. Try ejecting again when the save finishes." +msgstr "iOpenPod is still saving {label} to the iPod. Try ejecting again when the save finishes." + +#: GUI/app.py:1050 +msgid "iPod Ejected" +msgstr "iPod éjecté" + +#: GUI/app.py:958 +msgid "iPod Renamed" +msgstr "iPod renommé" + +#: GUI/app.py:1171 +msgid "quick changes" +msgstr "changements rapides" + +#: GUI/app.py:2290 +msgid "track" +msgstr "morceau" + +#: GUI/app.py:2290 GUI/widgets/playlistBrowser.py:1251 +msgid "tracks" +msgstr "morceaux" + +#: GUI/app.py:2293 +msgid "{count} more track was not copied." +msgstr "{count} morceau supplémentaire n’a pas été copié." + +#: GUI/app.py:2295 +msgid "{count} more tracks were not copied." +msgstr "{count} morceaux supplémentaires n’ont pas été copiés." + +#: GUI/app.py:2304 +msgid "{count} {tracks_word} were successfully copied to your iPod before the sync was cancelled." +msgstr "{count} {tracks_word} ont été copiés sur votre iPod avant l’annulation de la synchronisation." + +#: GUI/app.py:2544 +msgid "{tools} Not Found" +msgstr "{tools} introuvable" + +#: GUI/widgets/photoBrowser.py:1694 +msgid "Add Photo to Album" +msgstr "Ajouter une photo à l’album" + +#: GUI/widgets/photoBrowser.py:1246 GUI/widgets/photoBrowser.py:480 +msgid "Add to Album" +msgstr "Ajouter à l’album" + +#: GUI/widgets/photoBrowser.py:1676 +msgid "Album name:" +msgstr "Nom de l’album :" + +#: GUI/widgets/photoBrowser.py:1695 +msgid "Album:" +msgstr "Album :" + +#: GUI/widgets/photoBrowser.py:814 +msgid "All Photos" +msgstr "Toutes les photos" + +#: GUI/widgets/playlistBrowser.py:1012 +msgid "Choose a playlist to inspect its tracks and database metadata" +msgstr "Choisissez une liste pour inspecter ses morceaux et métadonnées" + +#: GUI/widgets/photoBrowser.py:1689 +msgid "Create another album first, or choose a photo that is not already in every album." +msgstr "Create another album first, or choose a photo that is not already in every album." + +#: GUI/widgets/playlistBrowser.py:437 +msgid "Delete" +msgstr "Supprimer" + +#: GUI/widgets/photoBrowser.py:1724 GUI/widgets/photoBrowser.py:1742 +msgid "Delete '{name}' from the iPod now?" +msgstr "Delete '{name}' from the iPod now?" + +#: GUI/widgets/photoBrowser.py:1323 GUI/widgets/photoBrowser.py:1723 +msgid "Delete Album" +msgstr "Supprimer l’album" + +#: GUI/widgets/photoBrowser.py:1267 GUI/widgets/photoBrowser.py:1741 GUI/widgets/photoBrowser.py:482 +msgid "Delete Photo" +msgstr "Supprimer la photo" + +#: GUI/widgets/playlistBrowser.py:561 +msgid "Details" +msgstr "Détails" + +#: GUI/widgets/playlistBrowser.py:451 GUI/widgets/playlistBrowser.py:1854 GUI/widgets/playlistBrowser.py:1879 +msgid "Evaluate Now" +msgstr "Évaluer maintenant" + +#: GUI/widgets/photoBrowser.py:479 GUI/widgets/playlistBrowser.py:473 +msgid "Export" +msgstr "Exporter" + +#: GUI/widgets/photoBrowser.py:1660 +msgid "Export Album" +msgstr "Exporter l’album" + +#: GUI/widgets/photoBrowser.py:1296 +msgid "Export Album..." +msgstr "Exporter l’album..." + +#: GUI/widgets/photoBrowser.py:1660 +msgid "Export All Photos" +msgstr "Exporter toutes les photos" + +#: GUI/widgets/photoBrowser.py:1296 +msgid "Export All Photos..." +msgstr "Exporter toutes les photos..." + +#: GUI/widgets/photoBrowser.py:1634 +msgid "Export Photo" +msgstr "Exporter la photo" + +#: GUI/widgets/photoBrowser.py:1238 +msgid "Export Photo..." +msgstr "Exporter la photo..." + +#: GUI/widgets/playlistBrowser.py:1338 GUI/widgets/playlistBrowser.py:1974 GUI/widgets/playlistBrowser.py:1603 +msgid "Import Playlist" +msgstr "Importer une liste de lecture" + +#: GUI/widgets/playlistBrowser.py:1401 +msgid "Importing Playlist…" +msgstr "Import de la liste…" + +#: GUI/widgets/playlistBrowser.py:1603 +msgid "Importing…" +msgstr "Import…" + +#: GUI/widgets/photoBrowser.py:423 GUI/widgets/photoBrowser.py:1305 GUI/widgets/photoBrowser.py:1676 +msgid "New Album" +msgstr "Nouvel album" + +#: GUI/widgets/photoBrowser.py:1710 +msgid "New album name:" +msgstr "Nom du nouvel album :" + +#: GUI/widgets/photoBrowser.py:1688 +msgid "No Available Albums" +msgstr "Aucun album disponible" + +#: GUI/widgets/photoBrowser.py:1574 GUI/widgets/photoBrowser.py:1650 +msgid "No Photos" +msgstr "Aucune photo" + +#: GUI/widgets/photoBrowser.py:1454 GUI/widgets/photoBrowser.py:1581 GUI/widgets/photoBrowser.py:1625 GUI/widgets/photoBrowser.py:1655 +msgid "No iPod Connected" +msgstr "Aucun iPod connecté" + +#: GUI/widgets/photoBrowser.py:474 +msgid "No photo selected" +msgstr "Aucune photo sélectionnée" + +#: GUI/widgets/playlistBrowser.py:1177 +msgid "No playlists on this iPod" +msgstr "Aucune liste de lecture sur cet iPod" + +#: GUI/widgets/playlistBrowser.py:1986 +msgid "Parsing playlist…" +msgstr "Analyse de la liste…" + +#: GUI/widgets/photoBrowser.py:481 +msgid "Remove from Album" +msgstr "Retirer de l’album" + +#: GUI/widgets/photoBrowser.py:1256 +msgid "Remove from Current Album" +msgstr "Retirer de l’album actuel" + +#: GUI/widgets/photoBrowser.py:1710 GUI/widgets/photoBrowser.py:1316 +msgid "Rename Album" +msgstr "Renommer l’album" + +#: GUI/widgets/playlistBrowser.py:528 +msgid "Rules" +msgstr "Règles" + +#: GUI/widgets/photoBrowser.py:475 +msgid "Select a photo to inspect its preview and album details." +msgstr "Sélectionnez une photo pour voir son aperçu et les détails de l’album." + +#: GUI/widgets/playlistBrowser.py:1513 GUI/widgets/playlistBrowser.py:1547 GUI/widgets/playlistBrowser.py:1811 GUI/widgets/playlistBrowser.py:384 GUI/widgets/playlistBrowser.py:1007 +msgid "Select a playlist" +msgstr "Sélectionner une liste de lecture" + +#: GUI/widgets/photoBrowser.py:1455 +msgid "Select an iPod before editing device photos." +msgstr "Sélectionnez un iPod avant de modifier les photos de l’appareil." + +#: GUI/widgets/photoBrowser.py:1582 GUI/widgets/photoBrowser.py:1626 GUI/widgets/photoBrowser.py:1656 +msgid "Select an iPod before exporting device photos." +msgstr "Sélectionnez un iPod avant d’exporter les photos de l’appareil." + +#: GUI/widgets/photoBrowser.py:1574 GUI/widgets/photoBrowser.py:1650 +msgid "There are no photos to export." +msgstr "Il n’y a aucune photo à exporter." + +#: GUI/widgets/playlistBrowser.py:1838 +msgid "Writing…" +msgstr "Écriture…" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "1" +msgstr "1" + +#: GUI/widgets/settingsPage.py:1950 +msgid "1 GB" +msgstr "1 GB" + +#: GUI/widgets/settingsPage.py:2008 GUI/widgets/settingsPage.py:2008 +msgid "10" +msgstr "10" + +#: GUI/widgets/settingsPage.py:1950 +msgid "10 GB" +msgstr "10 GB" + +#: GUI/widgets/settingsPage.py:1515 GUI/widgets/settingsPage.py:1515 +msgid "100%" +msgstr "100%" + +#: GUI/widgets/settingsPage.py:1515 +msgid "110%" +msgstr "110%" + +#: GUI/widgets/settingsPage.py:1515 +msgid "125%" +msgstr "125%" + +#: GUI/widgets/settingsPage.py:1716 +msgid "128 kbps" +msgstr "128 kbps" + +#: GUI/widgets/settingsPage.py:1787 +msgid "15 kHz" +msgstr "15 kHz" + +#: GUI/widgets/settingsPage.py:1515 +msgid "150%" +msgstr "150%" + +#: GUI/widgets/settingsPage.py:1787 +msgid "16 kHz" +msgstr "16 kHz" + +#: GUI/widgets/settingsPage.py:1716 +msgid "160 kbps" +msgstr "160 kbps" + +#: GUI/widgets/settingsPage.py:1787 +msgid "17 kHz" +msgstr "17 kHz" + +#: GUI/widgets/settingsPage.py:1751 +msgid "18 (High)" +msgstr "18 (High)" + +#: GUI/widgets/settingsPage.py:1787 +msgid "18 kHz" +msgstr "18 kHz" + +#: GUI/widgets/settingsPage.py:1787 +msgid "19 kHz" +msgstr "19 kHz" + +#: GUI/widgets/settingsPage.py:1716 GUI/widgets/settingsPage.py:1716 +msgid "192 kbps" +msgstr "192 kbps" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "2" +msgstr "2" + +#: GUI/widgets/settingsPage.py:1950 +msgid "2 GB" +msgstr "2 GB" + +#: GUI/widgets/settingsPage.py:2008 +msgid "20" +msgstr "20" + +#: GUI/widgets/settingsPage.py:1751 +msgid "20 (Good)" +msgstr "20 (Good)" + +#: GUI/widgets/settingsPage.py:1950 +msgid "20 GB" +msgstr "20 GB" + +#: GUI/widgets/settingsPage.py:1787 +msgid "20 kHz" +msgstr "20 kHz" + +#: GUI/widgets/settingsPage.py:1716 +msgid "224 kbps" +msgstr "224 kbps" + +#: GUI/widgets/settingsPage.py:1751 GUI/widgets/settingsPage.py:1751 +msgid "23 (Balanced)" +msgstr "23 (Balanced)" + +#: GUI/widgets/settingsPage.py:1716 +msgid "256 kbps" +msgstr "256 kbps" + +#: GUI/widgets/settingsPage.py:1751 +msgid "26 (Low)" +msgstr "26 (Low)" + +#: GUI/widgets/settingsPage.py:1751 +msgid "28 (Very Low)" +msgstr "28 (Very Low)" + +#: GUI/widgets/settingsPage.py:1728 +msgid "32 kbps" +msgstr "32 kbps" + +#: GUI/widgets/settingsPage.py:1716 +msgid "320 kbps" +msgstr "320 kbps" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "4" +msgstr "4" + +#: GUI/widgets/settingsPage.py:1728 +msgid "48 kbps" +msgstr "48 kbps" + +#: GUI/widgets/settingsPage.py:2008 +msgid "5" +msgstr "5" + +#: GUI/widgets/settingsPage.py:1950 GUI/widgets/settingsPage.py:1950 +msgid "5 GB" +msgstr "5 GB" + +#: GUI/widgets/settingsPage.py:1950 +msgid "50 GB" +msgstr "50 GB" + +#: GUI/widgets/settingsPage.py:1619 +msgid "6" +msgstr "6" + +#: GUI/widgets/settingsPage.py:1728 GUI/widgets/settingsPage.py:1728 +msgid "64 kbps" +msgstr "64 kbps" + +#: GUI/widgets/settingsPage.py:1515 +msgid "75%" +msgstr "75%" + +#: GUI/widgets/settingsPage.py:1619 +msgid "8" +msgstr "8" + +#: GUI/widgets/settingsPage.py:1728 +msgid "80 kbps" +msgstr "80 kbps" + +#: GUI/widgets/settingsPage.py:1515 +msgid "90%" +msgstr "90%" + +#: GUI/widgets/settingsPage.py:1716 GUI/widgets/settingsPage.py:1728 +msgid "96 kbps" +msgstr "96 kbps" + +msgid "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC.When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" +msgstr "Toujours sortir l’audio en 44,1 kHz (qualité CD). Recommandé pour les anciens iPods (1G-4G) qui peuvent avoir des problèmes avec l’ALAC 48 kHz. Désactivé, la fréquence est réduite à 48 kHz, car les iPods ne décodent que 48 kHz ou moins." + +#: GUI/widgets/settingsPage.py:1640 +msgid "Analyze loudness of files missing ReplayGain/iTunNORM tags using ffmpeg, then write the result back into your PC files and sync to iPod. Sound Check values are always synced to iPod regardless of this setting." +msgstr "Analyse avec ffmpeg le volume des fichiers sans tags ReplayGain/iTunNORM, puis réécrit le résultat dans les fichiers du PC et synchronise vers l’iPod. Les valeurs Sound Check sont toujours synchronisées vers l’iPod." + +#: GUI/widgets/settingsPage.py:1702 +msgid "Audio quality preset for music tracks. High: 256 kbps / best VBR. Balanced: 192 kbps. Compact: 128 kbps / smaller VBR." +msgstr "Préréglage de qualité audio pour la musique. Élevée : 256 kbps / meilleur VBR. Équilibrée : 192 kbps. Compacte : 128 kbps / VBR plus petit." + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 GUI/widgets/settingsPage.py:1627 GUI/widgets/settingsPage.py:1688 GUI/widgets/settingsPage.py:1688 GUI/widgets/settingsPage.py:1787 GUI/widgets/settingsPage.py:1787 +msgid "Auto" +msgstr "Auto" + +#: GUI/widgets/settingsPage.py:1902 +msgid "Automatically scrobble new iPod plays to connected services when you sync." +msgstr "Scrobbler automatiquement les nouvelles lectures iPod vers les services connectés lors de la synchronisation." + +#: GUI/widgets/settingsPage.py:1658 +msgid "Average" +msgstr "Moyenne" + +#: GUI/widgets/settingsPage.py:2001 +msgid "Backup Before Sync" +msgstr "Sauvegarde avant synchronisation" + +#: GUI/widgets/settingsPage.py:1995 +msgid "Backup Location" +msgstr "Emplacement de sauvegarde" + +#: GUI/widgets/settingsPage.py:1702 GUI/widgets/settingsPage.py:1702 +msgid "Balanced" +msgstr "Équilibré" + +#: GUI/widgets/settingsPage.py:1522 GUI/widgets/settingsPage.py:1522 +msgid "Blue (Default)" +msgstr "Bleu (par défaut)" + +#: GUI/widgets/settingsPage.py:1709 GUI/widgets/settingsPage.py:1709 +msgid "CBR" +msgstr "CBR" + +#: GUI/widgets/settingsPage.py:1709 +msgid "CBR uses a fixed target bitrate. VBR targets a quality level and lets the encoder choose bitrate per frame — typically better quality per byte." +msgstr "CBR utilise un débit cible fixe. VBR vise un niveau de qualité et laisse l’encodeur choisir le débit par trame, offrant généralement une meilleure qualité par octet." + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Frappé" +msgstr "Catppuccin Frappé" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Latte" +msgstr "Catppuccin Latte" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Macchiato" +msgstr "Catppuccin Macchiato" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Mocha" +msgstr "Catppuccin Mocha" + +#: GUI/widgets/settingsPage.py:2001 +msgid "Choose whether sync creates a full device backup automatically, asks each time, or skips pre-sync backups." +msgstr "Choisissez si la synchronisation crée automatiquement une sauvegarde complète, demande à chaque fois ou ignore les sauvegardes préalables." + +msgid "Choose which lossy encoder to use.Auto chooses the best available." +msgstr "Choisissez l’encodeur avec pertes à utiliser. Auto choisit le meilleur disponible." + +#: GUI/widgets/settingsPage.py:1702 +msgid "Compact" +msgstr "Compact" + +#: GUI/widgets/settingsPage.py:1918 +msgid "Connect your Last.fm account to scrobble iPod plays. You will need to provide your Last.fm API Key and API Secret." +msgstr "Connectez votre compte Last.fm pour scrobbler les lectures iPod. Vous devrez fournir la clé API et le secret API Last.fm." + +#: GUI/widgets/settingsPage.py:1908 +msgid "Connect your ListenBrainz account to scrobble iPod plays. Copy your user token from the link below." +msgstr "Connectez votre compte ListenBrainz pour scrobbler les lectures iPod. Copiez votre jeton utilisateur depuis le lien ci-dessous." + +#: GUI/widgets/settingsPage.py:1495 GUI/widgets/settingsPage.py:1495 +msgid "Dark" +msgstr "Sombre" + +#: GUI/widgets/settingsPage.py:1769 +msgid "Downmix to mono when encoding podcasts and audiobooks. Mono at lowbites sounds significantly better than stereo and cuts file sizes in half." +msgstr "Convertit en mono lors de l’encodage des podcasts et livres audio. À bas débit, le mono sonne nettement mieux que la stéréo et divise la taille par deux." + +msgid "Enable separate quality settings for podcasts and audiobooks.Music tracks are unaffected." +msgstr "Active des réglages de qualité séparés pour podcasts et livres audio. La musique n’est pas affectée." + +#: GUI/widgets/settingsPage.py:1795 +msgid "Enables a quality-enhancement post-processing pass in libfdk_aac. Improves output at the cost of slightly longer encode times. Only affects the libfdk_aac encoder." +msgstr "Enables a quality-enhancement post-processing pass in libfdk_aac. Improves output at the cost of slightly longer encode times. Only affects the libfdk_aac encoder." + +#: GUI/widgets/settingsPage.py:1735 +msgid "Encode lossless sources (ALAC, FLAC, WAV, AIFF) as your selected lossy format instead of ALAC. Saves iPod storage at the cost of quality." +msgstr "Encode les sources sans pertes (ALAC, FLAC, WAV, AIFF) dans le format avec pertes choisi au lieu d’ALAC. Économise l’espace iPod au prix de la qualité." + +#: GUI/widgets/settingsPage.py:1813 +msgid "Encodes stereo as Sum (Mid) and Difference (Side) rather than Left/Right. Concentrates bits where the signal is strongest, particularly on centred content. Affects the native aac encoder only." +msgstr "Encodes stereo as Sum (Mid) and Difference (Side) rather than Left/Right. Concentrates bits where the signal is strongest, particularly on centred content. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1647 +msgid "For portrait-heavy photos, rotate the device viewing caches clockwise when that uses more of the iPod's landscape photo screen. The original PC files are not modified." +msgstr "Pour les photos surtout verticales, fait pivoter les caches d’affichage de l’appareil quand cela utilise mieux l’écran paysage de l’iPod. Les fichiers originaux du PC ne sont pas modifiés." + +#: GUI/widgets/settingsPage.py:1741 +msgid "Force existing MP3 and AAC files through the selected lossy encoder. Use this to make the synced library more uniform or smaller." +msgstr "Force les fichiers MP3 et AAC existants à passer par l’encodeur avec pertes choisi. Utile pour rendre la bibliothèque synchronisée plus uniforme ou plus petite." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Gold" +msgstr "Or" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Green" +msgstr "Vert" + +#: GUI/widgets/settingsPage.py:1702 +msgid "High Quality" +msgstr "Haute qualité" + +#: GUI/widgets/settingsPage.py:1658 +msgid "Highest" +msgstr "Plus haute" + +#: GUI/widgets/settingsPage.py:1658 +msgid "How to resolve rating conflicts when iPod and PC ratings differ. iPod/PC Wins uses that source (falling back to the other if zero). Highest/Lowest picks the max/min non-zero value. Average rounds to the nearest star." +msgstr "Définit comment résoudre les conflits de notes entre iPod et PC. iPod/PC prioritaire utilise cette source (avec repli si zéro). Plus haute/plus basse choisit la valeur non nulle max/min. Moyenne arrondit à l’étoile la plus proche." + +#: GUI/widgets/settingsPage.py:1572 +msgid "Ko-fi" +msgstr "Ko-fi" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Light" +msgstr "Clair" + +#: GUI/widgets/settingsPage.py:1658 +msgid "Lowest" +msgstr "Plus basse" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Match iPod" +msgstr "Associer à l’iPod" + +#: GUI/widgets/settingsPage.py:2008 +msgid "Max Backups" +msgstr "Sauvegardes max." + +#: GUI/widgets/settingsPage.py:1787 +msgid "Maximum frequency the encoder will output. Lowering this (16–18 kHz) frees bits for the mid-range and can eliminate high-frequency squeaks at lower bitrates. Auto lets the encoder decide. Applies to all AAC encoders." +msgstr "Maximum frequency the encoder will output. Lowering this (16–18 kHz) frees bits for the mid-range and can eliminate high-frequency squeaks at lower bitrates. Auto lets the encoder decide. Applies to all AAC encoders." + +#: GUI/widgets/settingsPage.py:2008 +msgid "Maximum number of backup snapshots to keep per device. Oldest backups are automatically removed when the limit is exceeded." +msgstr "Nombre maximal d’instantanés de sauvegarde à conserver par appareil. Les plus anciens sont supprimés automatiquement quand la limite est dépassée." + +#: GUI/widgets/settingsPage.py:1819 +msgid "Merges high-frequency stereo bands into a single channel with direction metadata. Saves bits at low bitrates but can cause stereo image wobble. Affects the native aac encoder only." +msgstr "Merges high-frequency stereo bands into a single channel with direction metadata. Saves bits at low bitrates but can cause stereo image wobble. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1507 GUI/widgets/settingsPage.py:1507 +msgid "Off" +msgstr "Désactivé" + +#: GUI/widgets/settingsPage.py:1507 +msgid "On" +msgstr "Activé" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Orange" +msgstr "Orange" + +#: GUI/widgets/settingsPage.py:1619 +msgid "Overall concurrent sync work. This controls how many files can be prepared, fingerprinted, transcoded, or copied at once. Auto uses your CPU core count (capped at 8)." +msgstr "Travail de synchronisation concurrent global. Contrôle combien de fichiers peuvent être préparés, empreintés, transcodés ou copiés à la fois. Auto utilise le nombre de cœurs CPU (max. 8)." + +#: GUI/widgets/settingsPage.py:1658 +msgid "PC Wins" +msgstr "PC prioritaire" + +#: GUI/widgets/settingsPage.py:1893 +msgid "Path Overrides" +msgstr "Remplacements de chemin" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Pink" +msgstr "Rose" + +#: GUI/widgets/settingsPage.py:1882 +msgid "Point to a custom ffmpeg binary. Leave empty to auto-detect." +msgstr "Indiquez un binaire ffmpeg personnalisé. Laissez vide pour détecter automatiquement." + +#: GUI/widgets/settingsPage.py:1887 +msgid "Point to a custom fpcalc binary. Leave empty to auto-detect." +msgstr "Indiquez un binaire fpcalc personnalisé. Laissez vide pour détecter automatiquement." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Purple" +msgstr "Violet" + +#: GUI/widgets/settingsPage.py:1751 +msgid "Quality level for H.264 video transcodes. Lower CRF = better quality but larger files. Resolution and codec are always forced to iPod-compatible values." +msgstr "Niveau de qualité pour les transcodages vidéo H.264. CRF plus bas = meilleure qualité mais fichiers plus gros. La résolution et le codec sont toujours forcés pour l’iPod." + +#: GUI/widgets/settingsPage.py:1722 +msgid "Quality level for VBR encoding. Higher = better quality, larger files." +msgstr "Niveau de qualité pour l’encodage VBR. Plus élevé = meilleure qualité, fichiers plus gros." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Red" +msgstr "Rouge" + +#: GUI/widgets/settingsPage.py:1807 +msgid "Replaces noise-like frequency bands with synthetic noise, saving bits. Can cause sandpaper or hissing artifacts if the encoder mistakes tonal content for noise. Off by default. Affects the native aac encoder only." +msgstr "Replaces noise-like frequency bands with synthetic noise, saving bits. Can cause sandpaper or hissing artifacts if the encoder mistakes tonal content for noise. Off by default. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1875 +msgid "Required for acoustic fingerprinting, which identifies tracks even after re-encoding." +msgstr "Requis pour l’empreinte acoustique, qui identifie les morceaux même après réencodage." + +#: GUI/widgets/settingsPage.py:1869 +msgid "Required for transcoding and media probing. Includes ffmpeg and ffprobe." +msgstr "Requis pour le transcodage et l’analyse des médias. Inclut ffmpeg et ffprobe." + +#: GUI/widgets/settingsPage.py:1902 +msgid "Scrobble on Sync" +msgstr "Scrobbler à la synchronisation" + +#: GUI/widgets/settingsPage.py:1928 +msgid "Services" +msgstr "Services" + +#: GUI/widgets/settingsPage.py:1801 +msgid "Shapes quantization noise around transients to reduce pre-echo (smearing before drum hits). Disabling saves a few bits per block. Affects the native aac encoder only." +msgstr "Shapes quantization noise around transients to reduce pre-echo (smearing before drum hits). Disabling saves a few bits per block. Affects the native aac encoder only." + +#: GUI/widgets/settingsPage.py:1627 +msgid "Simultaneous writes to the iPod filesystem. Set to 1 for HDD-based iPods to reduce fragmentation risk. Auto uses an HDD-safe default when the device looks like a hard-drive iPod." +msgstr "Écritures simultanées sur le système de fichiers iPod. Mettre à 1 pour les iPods à disque dur afin de réduire la fragmentation. Auto utilise une valeur sûre pour HDD quand l’appareil semble être un iPod à disque." + +#: GUI/widgets/settingsPage.py:1762 +msgid "Slower presets produce slightly better quality at the same CRF, but take much longer." +msgstr "Les préréglages plus lents offrent une qualité légèrement meilleure au même CRF, mais prennent beaucoup plus de temps." + +#: GUI/widgets/podcastBrowser.py:1566 +msgid "Subscriptions" +msgstr "Abonnements" + +#: GUI/widgets/settingsPage.py:1495 GUI/widgets/settingsPage.py:1507 +msgid "System" +msgstr "Système" + +#: GUI/widgets/settingsPage.py:1716 +msgid "Target CBR bitrate for music tracks." +msgstr "Débit CBR cible pour les morceaux de musique." + +#: GUI/widgets/settingsPage.py:1728 +msgid "Target bitrate for podcasts and audiobooks (Always uses CBR despite set Bitrate Mode). Pair with 'Mono for Spoken Word' for best results." +msgstr "Débit cible pour podcasts et livres audio (utilise toujours CBR malgré le mode choisi). À associer avec « Mono pour voix parlée » pour de meilleurs résultats." + +#: GUI/widgets/settingsPage.py:1522 +msgid "Teal" +msgstr "Bleu sarcelle" + +#: GUI/widgets/settingsPage.py:1950 GUI/widgets/settingsPage.py:2008 +msgid "Unlimited" +msgstr "Illimité" + +#: GUI/widgets/settingsPage.py:1653 +msgid "Use aspect-fit for device photo thumbnail formats. When off (default), thumbnails use iTunes-style crop-to-fill." +msgstr "Utilise l’ajustement proportionnel pour les miniatures photo de l’appareil. Désactivé (par défaut), les miniatures utilisent un recadrage façon iTunes." + +#: GUI/widgets/settingsPage.py:1709 +msgid "VBR" +msgstr "VBR" + +#: GUI/widgets/settingsPage.py:1746 +msgid "When enabled, WAV files are converted to ALAC instead of copied. Prefer Lossy Encoding overrides this and converts WAV to the selected lossy format." +msgstr "Quand activé, les fichiers WAV sont convertis en ALAC au lieu d’être copiés. « Préférer l’encodage avec pertes » remplace ce réglage et convertit WAV au format avec pertes choisi." + +#: GUI/widgets/settingsPage.py:1995 +msgid "Where full device backups are stored on your PC. Leave empty for the platform default." +msgstr "Emplacement des sauvegardes complètes de l’appareil sur le PC. Laissez vide pour la valeur par défaut." + +#: GUI/widgets/settingsPage.py:1635 +msgid "While syncing, write ratings and sound check values into your PC music files. When off, no changes are made to your PC files." +msgstr "Pendant la synchronisation, écrit les notes et valeurs Sound Check dans les fichiers musicaux du PC. Désactivé, les fichiers du PC ne sont pas modifiés." + +#: GUI/widgets/settingsPage.py:1688 +msgid "aac" +msgstr "aac" + +#: GUI/widgets/settingsPage.py:1688 +msgid "aac_at" +msgstr "aac_at" + +#: GUI/widgets/settingsPage.py:1488 +msgid "en" +msgstr "en" + +#: GUI/widgets/settingsPage.py:1762 GUI/widgets/settingsPage.py:1762 +msgid "fast" +msgstr "fast" + +#: GUI/widgets/settingsPage.py:1658 GUI/widgets/settingsPage.py:1658 +msgid "iPod Wins" +msgstr "iPod prioritaire" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libfdk_aac" +msgstr "libfdk_aac" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libmp3lame" +msgstr "libmp3lame" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libshine" +msgstr "libshine" + +#: GUI/widgets/settingsPage.py:1762 +msgid "medium" +msgstr "medium" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q0 (Best)" +msgstr "q0 (meilleur)" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q1" +msgstr "q1" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q2" +msgstr "q2" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q3" +msgstr "q3" + +#: GUI/widgets/settingsPage.py:1722 GUI/widgets/settingsPage.py:1722 +msgid "q4" +msgstr "q4" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q5" +msgstr "q5" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q6" +msgstr "q6" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q7" +msgstr "q7" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q8" +msgstr "q8" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q9 (Smallest)" +msgstr "q9 (plus petit)" + +#: GUI/widgets/settingsPage.py:1762 +msgid "slow" +msgstr "slow" + +#: GUI/widgets/settingsPage.py:1762 +msgid "ultrafast" +msgstr "ultrafast" + +#: GUI/widgets/settingsPage.py:1762 +msgid "veryfast" +msgstr "veryfast" + +#: GUI/widgets/settingsPage.py:1780 +msgid "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC. When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" +msgstr "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC. When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" + +#: GUI/widgets/settingsPage.py:1688 +msgid "Choose which lossy encoder to use. Auto chooses the best available." +msgstr "Choisissez l’encodeur avec pertes à utiliser. Auto choisit le meilleur disponible." + +#: GUI/widgets/settingsPage.py:1775 +msgid "Enable separate quality settings for podcasts and audiobooks. Music tracks are unaffected." +msgstr "Active des réglages de qualité séparés pour podcasts et livres audio. La musique n’est pas affectée." + +#: GUI/widgets/playlistBrowser.py:1667 +msgid "New Smart Playlist" +msgstr "Nouvelle liste intelligente" + +#: GUI/widgets/podcastBrowser.py:1441 +msgid "Add Podcast" +msgstr "Ajouter un podcast" + +#: GUI/widgets/podcastBrowser.py:1531 +msgid "Add Your First Podcast" +msgstr "Ajouter votre premier podcast" + +#: GUI/widgets/podcastBrowser.py:451 +msgid "Add this episode to iPod" +msgstr "Ajouter cet épisode à l’iPod" + +#: GUI/widgets/podcastBrowser.py:469 GUI/widgets/podcastBrowser.py:449 GUI/widgets/podcastBrowser.py:2102 +msgid "Add to iPod" +msgstr "Ajouter à l’iPod" + +#: GUI/widgets/podcastBrowser.py:1469 +msgid "Apply per-feed settings: remove listened/old episodes, fill empty slots with new episodes" +msgstr "Appliquer les réglages du flux : retirer les épisodes écoutés/anciens et remplir les emplacements libres avec de nouveaux épisodes" + +#: GUI/widgets/podcastBrowser.py:235 +msgid "Downloaded" +msgstr "Téléchargé" + +#: GUI/widgets/podcastBrowser.py:2247 +msgid "Downloaded: {count}" +msgstr "Téléchargés : {count}" + +#: GUI/widgets/podcastBrowser.py:260 +msgid "Episode" +msgstr "Épisode" + +#: GUI/widgets/podcastBrowser.py:2244 +msgid "Episodes: {count}" +msgstr "Épisodes : {count}" + +#: GUI/widgets/podcastBrowser.py:516 GUI/widgets/podcastBrowser.py:596 GUI/widgets/podcastBrowser.py:525 +msgid "More" +msgstr "Plus" + +#: GUI/widgets/podcastBrowser.py:1508 +msgid "No Podcast Subscriptions" +msgstr "Aucun abonnement aux podcasts" + +#: GUI/widgets/podcastBrowser.py:586 +msgid "No description available." +msgstr "Aucune description disponible." + +#: GUI/widgets/podcastBrowser.py:233 +msgid "On iPod" +msgstr "Sur l’iPod" + +#: GUI/widgets/podcastBrowser.py:2250 +msgid "On iPod: {count}" +msgstr "Sur l’iPod : {count}" + +#: GUI/widgets/podcastBrowser.py:2240 +msgid "RSS feed linked" +msgstr "Flux RSS lié" + +#: GUI/widgets/podcastBrowser.py:1451 +msgid "Refresh All" +msgstr "Tout actualiser" + +#: GUI/widgets/podcastBrowser.py:2024 +msgid "Refresh Feed" +msgstr "Actualiser le flux" + +#: GUI/widgets/podcastBrowser.py:2109 +msgid "Remove Download" +msgstr "Supprimer le téléchargement" + +#: GUI/widgets/podcastBrowser.py:504 GUI/widgets/podcastBrowser.py:480 GUI/widgets/podcastBrowser.py:2116 +msgid "Remove from iPod" +msgstr "Retirer de l’iPod" + +#: GUI/widgets/podcastBrowser.py:484 +msgid "Remove this episode from iPod" +msgstr "Retirer cet épisode de l’iPod" + +#: GUI/widgets/podcastBrowser.py:1518 +msgid "" +"Search for podcasts or add an RSS feed to get started.\n" +"Episodes can be downloaded and synced to your iPod." +msgstr "" +"Recherchez des podcasts ou ajoutez un flux RSS pour commencer.\n" +"Les épisodes peuvent être téléchargés et synchronisés avec votre iPod." + +#: GUI/widgets/podcastBrowser.py:1648 GUI/widgets/podcastBrowser.py:2209 +msgid "Select a podcast" +msgstr "Sélectionner un podcast" + +#: GUI/widgets/podcastBrowser.py:596 GUI/widgets/podcastBrowser.py:526 +msgid "Show less" +msgstr "Afficher moins" + +#: GUI/widgets/podcastBrowser.py:1461 +msgid "Sync Podcasts" +msgstr "Synchroniser les podcasts" + +#: GUI/widgets/podcastBrowser.py:2226 +msgid "Unknown Author" +msgstr "Auteur inconnu" + +#: GUI/widgets/podcastBrowser.py:2026 +msgid "Unsubscribe" +msgstr "Se désabonner" + +#: GUI/widgets/podcastBrowser.py:637 GUI/widgets/podcastBrowser.py:584 +msgid "Untitled Episode" +msgstr "Épisode sans titre" + +#: GUI/widgets/podcastBrowser.py:2225 +msgid "Untitled Podcast" +msgstr "Podcast sans titre" + +#: GUI/widgets/podcastBrowser.py:2238 +msgid "Updated {date}" +msgstr "Mis à jour {date}" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Day" +msgstr "1 jour" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Month" +msgstr "1 mois" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Week" +msgstr "1 semaine" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "2 Months" +msgstr "2 mois" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "2 Weeks" +msgstr "2 semaines" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "3 Days" +msgstr "3 jours" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "3 Months" +msgstr "3 mois" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "Adding podcast…" +msgstr "Ajout du podcast…" + +#: GUI/widgets/podcastBrowser.py:2547 +msgid "All podcasts are up to date" +msgstr "Tous les podcasts sont à jour" + +#: GUI/widgets/podcastBrowser.py:2825 +msgid "All selected episodes are already on iPod" +msgstr "Tous les épisodes sélectionnés sont déjà sur l’iPod" + +#: GUI/widgets/podcastBrowser.py:2581 +msgid "Already subscribed" +msgstr "Déjà abonné" + +#: GUI/widgets/podcastSearchDialog.py:192 +msgid "Checking podcast results now." +msgstr "Vérification des résultats de podcasts." + +#: GUI/widgets/podcastBrowser.py:2371 +msgid "Checking subscribed feeds for new episodes." +msgstr "Recherche de nouveaux épisodes dans les flux abonnés." + +#: GUI/widgets/podcastBrowser.py:2671 +msgid "Checking the feed for the latest episodes." +msgstr "Recherche des derniers épisodes dans le flux." + +#: GUI/widgets/podcastBrowser.py:1775 +msgid "Clear method:" +msgstr "Méthode de nettoyage :" + +#: GUI/widgets/podcastBrowser.py:1777 +msgid "Clear older than:" +msgstr "Supprimer au-delà de :" + +#: GUI/widgets/podcastBrowser.py:1776 +msgid "Clear when listened:" +msgstr "Supprimer après écoute :" + +#: GUI/widgets/podcastBrowser.py:2657 +msgid "Could not add podcast" +msgstr "Impossible d’ajouter le podcast" + +#: GUI/widgets/podcastBrowser.py:2931 +msgid "Episodes not found on iPod" +msgstr "Épisodes introuvables sur l’iPod" + +#: GUI/widgets/podcastBrowser.py:1773 +msgid "Episodes:" +msgstr "Épisodes :" + +#: GUI/widgets/podcastBrowser.py:2584 +msgid "Fetching feed…" +msgstr "Récupération du flux…" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "Fetching the feed and latest episodes." +msgstr "Récupération du flux et des derniers épisodes." + +#: GUI/widgets/podcastBrowser.py:1774 +msgid "Fill with:" +msgstr "Remplir avec :" + +#: GUI/widgets/podcastSearchDialog.py:118 +msgid "Find podcasts" +msgstr "Rechercher des podcasts" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "Immediately" +msgstr "Immédiatement" + +#: GUI/widgets/podcastBrowser.py:2330 +msgid "Loading episodes…" +msgstr "Chargement des épisodes…" + +#: GUI/widgets/podcastBrowser.py:1765 +msgid "Mark for Replacement" +msgstr "Marquer pour remplacement" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "Never" +msgstr "Jamais" + +#: GUI/widgets/podcastBrowser.py:1764 +msgid "Newest Episode" +msgstr "Épisode le plus récent" + +#: GUI/widgets/podcastBrowser.py:1764 +msgid "Next Episode" +msgstr "Épisode suivant" + +#: GUI/widgets/podcastBrowser.py:2281 +msgid "No episodes found" +msgstr "Aucun épisode trouvé" + +#: GUI/widgets/podcastBrowser.py:2815 +msgid "No episodes to sync" +msgstr "Aucun épisode à synchroniser" + +#: GUI/widgets/podcastBrowser.py:2772 GUI/widgets/podcastBrowser.py:2899 +msgid "No iPod connected" +msgstr "Aucun iPod connecté" + +#: GUI/widgets/podcastSearchDialog.py:213 +msgid "No podcasts found" +msgstr "Aucun podcast trouvé" + +#: GUI/widgets/podcastBrowser.py:2366 +msgid "No subscriptions to refresh" +msgstr "Aucun abonnement à actualiser" + +#: GUI/widgets/podcastBrowser.py:2476 +msgid "No subscriptions to sync" +msgstr "Aucun abonnement à synchroniser" + +#: GUI/widgets/podcastBrowser.py:2446 +msgid "Podcasts could not refresh" +msgstr "Impossible d’actualiser les podcasts" + +#: GUI/widgets/podcastBrowser.py:2481 +msgid "Preparing podcast sync…" +msgstr "Préparation de la synchronisation des podcasts…" + +#: GUI/widgets/podcastBrowser.py:2457 +msgid "Refresh failed" +msgstr "Échec de l’actualisation" + +#: GUI/widgets/podcastBrowser.py:2481 +msgid "Refreshing feeds before building the sync plan." +msgstr "Actualisation des flux avant la création du plan de synchronisation." + +#: GUI/widgets/podcastBrowser.py:2480 +msgid "Refreshing feeds for sync…" +msgstr "Actualisation des flux pour la synchronisation…" + +#: GUI/widgets/podcastBrowser.py:2371 +msgid "Refreshing podcasts…" +msgstr "Actualisation des podcasts…" + +#: GUI/widgets/podcastBrowser.py:2671 +msgid "Refreshing this podcast…" +msgstr "Actualisation de ce podcast…" + +#: GUI/widgets/podcastBrowser.py:1765 +msgid "Remove Immediately" +msgstr "Supprimer immédiatement" + +#: GUI/widgets/podcastSearchDialog.py:118 +msgid "Search by show name, or paste an RSS feed below." +msgstr "Recherchez par nom d’émission ou collez un flux RSS ci-dessous." + +#: GUI/widgets/podcastSearchDialog.py:192 +msgid "Searching for podcasts…" +msgstr "Recherche de podcasts…" + +#: GUI/widgets/podcastBrowser.py:2756 +msgid "Select episodes first" +msgstr "Sélectionnez d’abord des épisodes" + +#: GUI/widgets/podcastBrowser.py:2784 +msgid "Selected episodes are already on iPod" +msgstr "Les épisodes sélectionnés sont déjà sur l’iPod" + +#: GUI/widgets/podcastBrowser.py:2786 +msgid "Selected episodes cannot be added yet" +msgstr "Les épisodes sélectionnés ne peuvent pas encore être ajoutés" + +#: GUI/widgets/podcastBrowser.py:2448 +msgid "Some podcasts could not refresh" +msgstr "Certains podcasts n’ont pas pu être actualisés" + +#: GUI/widgets/podcastBrowser.py:2196 +msgid "Subscribed shows will appear here after their feeds refresh." +msgstr "Les émissions abonnées apparaîtront ici après l’actualisation des flux." + +#: GUI/widgets/podcastBrowser.py:2569 +msgid "Sync failed" +msgstr "Échec de la synchronisation" + +#: GUI/widgets/podcastBrowser.py:2471 GUI/widgets/podcastBrowser.py:2769 +msgid "This iPod does not support podcasts" +msgstr "Cet iPod ne prend pas en charge les podcasts" + +#: GUI/widgets/podcastBrowser.py:2281 +msgid "This podcast loaded, but its feed did not list any episodes." +msgstr "Ce podcast s’est chargé, mais son flux ne contient aucun épisode." + +#: GUI/widgets/podcastSearchDialog.py:213 +msgid "Try a different show name, or paste the RSS feed directly." +msgstr "Essayez un autre nom d’émission ou collez directement le flux RSS." + +#: GUI/widgets/podcastBrowser.py:2196 +msgid "Waiting for episodes" +msgstr "En attente d’épisodes" + + +#: GUI/widgets/podcastBrowser.py:2895 GUI/widgets/podcastBrowser.py:2896 +msgid "Failed: {error}" +msgstr "Échec : {error}" + +#: GUI/widgets/podcastBrowser.py:2593 GUI/widgets/podcastBrowser.py:2594 +msgid "Podcast sync: {summary}" +msgstr "Synchronisation des podcasts : {summary}" + +#: GUI/widgets/podcastBrowser.py:2425 +msgid "Refreshed {count} feed" +msgstr "{count} flux actualisé" + +#: GUI/widgets/podcastBrowser.py:2427 +msgid "Refreshed {count} feeds" +msgstr "{count} flux actualisés" + +#: GUI/widgets/podcastBrowser.py:2454 +msgid "Refreshed {count}; {failures} feed could not update" +msgstr "{count} actualisé ; {failures} flux n’a pas pu être actualisé" + +#: GUI/widgets/podcastBrowser.py:2459 +msgid "Refreshed {count}; {failures} feeds could not update" +msgstr "{count} actualisés ; {failures} flux n’ont pas pu être actualisés" + +#: GUI/widgets/podcastBrowser.py:2745 GUI/widgets/podcastBrowser.py:2746 +msgid "Refreshed {title}" +msgstr "{title} actualisé" + +#: GUI/widgets/podcastBrowser.py:2374 +msgid "Refreshing {count} feeds…" +msgstr "Actualisation de {count} flux…" + +#: GUI/widgets/podcastBrowser.py:2372 +msgid "Refreshing {count} feed…" +msgstr "Actualisation de {count} flux…" + +#: GUI/widgets/podcastBrowser.py:2715 GUI/widgets/podcastBrowser.py:2716 +msgid "Refreshing {title}…" +msgstr "Actualisation de {title}…" + +#: GUI/widgets/podcastBrowser.py:2942 +msgid "Removed {count} download" +msgstr "{count} téléchargement supprimé" + +#: GUI/widgets/podcastBrowser.py:2944 +msgid "Removed {count} downloads" +msgstr "{count} téléchargements supprimés" + +#: GUI/widgets/podcastBrowser.py:2881 +msgid "Sending {count} episode to sync…" +msgstr "Envoi de {count} épisode à synchroniser…" + +#: GUI/widgets/podcastBrowser.py:2883 +msgid "Sending {count} episodes to sync…" +msgstr "Envoi de {count} épisodes à synchroniser…" + +#: GUI/widgets/podcastBrowser.py:3005 +msgid "Sending {count} removal to sync…" +msgstr "Envoi de {count} suppression à synchroniser…" + +#: GUI/widgets/podcastBrowser.py:3007 +msgid "Sending {count} removals to sync…" +msgstr "Envoi de {count} suppressions à synchroniser…" + +#: GUI/widgets/podcastBrowser.py:2675 GUI/widgets/podcastBrowser.py:2676 +msgid "Subscribed to {title}" +msgstr "Abonné à {title}" + +#: GUI/widgets/podcastBrowser.py:2705 GUI/widgets/podcastBrowser.py:2706 +msgid "Unsubscribed from {title}" +msgstr "Désabonné de {title}" + +#: GUI/widgets/podcastBrowser.py:2590 +msgid "{count} to add" +msgstr "{count} à ajouter" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "{count} to remove" +msgstr "{count} à supprimer" + +#: GUI/widgets/playlistBrowser.py +msgid "Music" +msgstr "Musique" + +#: GUI/widgets/playlistBrowser.py +msgid "Ringtones" +msgstr "Sonneries" + +#: GUI/widgets/playlistBrowser.py +msgid "Rentals" +msgstr "Locations" diff --git a/locale/zh_CN/LC_MESSAGES/iopenpod.mo b/locale/zh_CN/LC_MESSAGES/iopenpod.mo new file mode 100644 index 0000000..7e3f437 Binary files /dev/null and b/locale/zh_CN/LC_MESSAGES/iopenpod.mo differ diff --git a/locale/zh_CN/LC_MESSAGES/iopenpod.po b/locale/zh_CN/LC_MESSAGES/iopenpod.po new file mode 100644 index 0000000..4479452 --- /dev/null +++ b/locale/zh_CN/LC_MESSAGES/iopenpod.po @@ -0,0 +1,2763 @@ +# Chinese translations for iOpenPod. +msgid "" +msgstr "" +"Project-Id-Version: iOpenPod 1.0.62\n" +"" +"Report-Msgid-Bugs-To: https://github.com/TheRealSavi/iOpenPod/issues\n" +"" +"POT-Creation-Date: 2026-06-30 00:00+0800\n" +"" +"PO-Revision-Date: 2026-06-30 00:00+0800\n" +"" +"Last-Translator: iOpenPod contributors\n" +"" +"Language-Team: Chinese\n" +"" +"Language: zh_CN\n" +"" +"MIME-Version: 1.0\n" +"" +"Content-Type: text/plain; charset=UTF-8\n" +"" +"Content-Transfer-Encoding: 8bit\n" +"" + +msgid "A crash report has been saved to:\n" +"" +msgstr "崩溃报告已保存到:\n" +"" + +msgid "API Key" +msgstr "API 密钥" + +msgid "API Key and Secret required" +msgstr "需要 API Key 和 Secret" + +msgid "API Secret" +msgstr "API 密钥 Secret" + +msgid "All Tracks" +msgstr "全部曲目" + +msgid "About" +msgstr "关于" + +msgid "Accent Color" +msgstr "强调色" + +msgid "Advanced AAC" +msgstr "高级 AAC" + +msgid "Albums" +msgstr "专辑" + +msgid "Album" +msgstr "专辑" + +msgid "An unexpected error occurred:\n" +"\n" +"" +msgstr "发生未预期的错误:\n" +"\n" +"" + +msgid "Appearance" +msgstr "外观" + +msgid "Apply a subtle display-only sharpening pass to album art in grid cards and track lists. This does not modify artwork written to your iPod." +msgstr "在网格卡片和曲目列表中对专辑封面应用轻微的仅显示锐化处理。不会修改写入 iPod 的封面。" + +msgid "Artists" +msgstr "艺人" + +msgid "Artist" +msgstr "艺人" + +msgid "Audio" +msgstr "音频" + +msgid "Audiobooks" +msgstr "有声书" + +msgid "Auto-detect" +msgstr "自动检测" + +msgid "Ask Each Time" +msgstr "每次询问" + +msgid "Back" +msgstr "返回" + +msgid "Backup Folder" +msgstr "备份文件夹" + +msgid "Backups" +msgstr "备份" + +msgid "Backups and Rollback" +msgstr "备份与回滚" + +msgid "Bandwidth Cutoff" +msgstr "频宽截止" + +msgid "Before Sync" +msgstr "同步前" + +msgid "Behavior" +msgstr "行为" + +msgid "Duration" +msgstr "时长" + +msgid "Bitrate Mode" +msgstr "码率模式" + +msgid "Boost text and border contrast for accessibility. System follows your OS accessibility setting." +msgstr "提高文本和边框对比度以改善可访问性。“系统”会跟随操作系统可访问性设置。" + +msgid "Browse..." +msgstr "浏览..." + +msgid "Browse…" +msgstr "浏览…" + +msgid "Cache Status" +msgstr "缓存状态" + +msgid "Calculating…" +msgstr "正在计算…" + +msgid "Cancel" +msgstr "取消" + +msgid "Canceled" +msgstr "已取消" + +msgid "Check" +msgstr "检查" + +msgid "Check for a newer version of iOpenPod." +msgstr "检查是否有新版 iOpenPod。" + +msgid "Checking…" +msgstr "正在检查…" + +msgid "Choose the color scheme for the interface. System follows your OS preference." +msgstr "选择界面的配色方案。“系统”会跟随操作系统偏好。" + +msgid "Choose the interface language. Changing it rebuilds the window." +msgstr "选择界面语言。更改后会重建窗口以应用新语言。" + +msgid "Clear Cache" +msgstr "清除缓存" + +msgid "Clear Transcode Cache" +msgstr "清除转码缓存" + +msgid "Click to rename your iPod" +msgstr "点击重命名你的 iPod" + +msgid "Compute Sound Check" +msgstr "计算音量均衡" + +msgid "Connect" +msgstr "连接" + +msgid "Connected as" +msgstr "已连接为" + +msgid "Convert WAV to ALAC" +msgstr "将 WAV 转为 ALAC" + +msgid "Copy the current global values into this iPod's settings file." +msgstr "将当前全局值复制到这个 iPod 的设置文件。" + +msgid "Customize the accent color used throughout the interface. Match iPod uses the body color of your connected iPod." +msgstr "自定义界面强调色。“匹配 iPod”会使用当前连接 iPod 的机身颜色。" + +msgid "Delete all cached transcoded files?\n" +"\n" +"They will be re-created on the next sync." +msgstr "删除所有缓存的转码文件?\n" +"\n" +"它们会在下次同步时重新创建。" + +msgid "Device" +msgstr "设备" + +msgid "Device..." +msgstr "设备..." + +msgid "Disconnect" +msgstr "断开连接" + +msgid "Download" +msgstr "下载" + +msgid "Downloading…" +msgstr "正在下载…" + +msgid "External Tools" +msgstr "外部工具" + +msgid "FFmpeg" +msgstr "FFmpeg" + +msgid "FFmpeg Path Override" +msgstr "FFmpeg 路径覆盖" + +msgid "Fetching token..." +msgstr "正在获取令牌..." + +msgid "Fit Thumbnails" +msgstr "适配缩略图" + +msgid "Font Size" +msgstr "字体大小" + +msgid "General" +msgstr "通用" + +msgid "Genres" +msgstr "流派" + +msgid "Get API keys ↗" +msgstr "获取 API 密钥 ↗" + +msgid "Get token ↗" +msgstr "获取令牌 ↗" + +msgid "Global" +msgstr "全局" + +msgid "Hours" +msgstr "小时" + +msgid "Ignore this iPod's settings file and use the PC settings while this iPod is selected." +msgstr "选择此 iPod 时忽略它的设备设置文件,改用电脑上的全局设置。" + +msgid "Increased Contrast" +msgstr "增强对比度" + +msgid "Intensity Stereo" +msgstr "强度立体声" + +msgid "Invalid iPod Folder" +msgstr "无效的 iPod 文件夹" + +msgid "Language" +msgstr "语言" + +msgid "Last.fm" +msgstr "Last.fm" + +msgid "Library" +msgstr "资料库" + +msgid "LIBRARY" +msgstr "资料库" + +msgid "Library (Master)" +msgstr "资料库(主)" + +msgid "ListenBrainz" +msgstr "ListenBrainz" + +msgid "Loading device settings..." +msgstr "正在载入设备设置..." + +msgid "Loading iPod..." +msgstr "正在载入 iPod..." + +msgid "Log Folder" +msgstr "日志文件夹" + +msgid "Lossy Encoder" +msgstr "有损编码器" + +msgid "Manage" +msgstr "管理" + +msgid "Maximum Backups" +msgstr "最大备份数" + +msgid "Mid/Side Stereo" +msgstr "中/侧声道立体声" + +msgid "Mono for Spoken Word" +msgstr "语音内容转单声道" + +msgid "Movies" +msgstr "电影" + +msgid "Music Bitrate" +msgstr "音乐码率" + +msgid "Music Quality" +msgstr "音乐质量" + +msgid "Music Videos" +msgstr "音乐视频" + +msgid "No" +msgstr "否" + +msgid "No Device" +msgstr "无设备" + +msgid "No device is currently selected.\n" +"Choose an iPod to access your library and sync tools." +msgstr "当前未选择设备。\n" +"选择一个 iPod 后即可访问资料库和同步工具。" + +msgid "None" +msgstr "无" + +msgid "Name" +msgstr "名称" + +msgid "Normalize Tags" +msgstr "规范化标签" + +msgid "Normalize to 44.1 kHz" +msgstr "规范化为 44.1 kHz" + +msgid "Not found" +msgstr "未找到" + +msgid "Not set" +msgstr "未设置" + +msgid "Open" +msgstr "打开" + +msgid "Open the GitHub issue tracker to report problems or request features." +msgstr "打开 GitHub issue tracker 以报告问题或提出功能请求。" + +msgid "Open ↗" +msgstr "打开 ↗" + +msgid "Overridden by device settings" +msgstr "已被设备设置覆盖" + +msgid "Parallel Workers" +msgstr "并行任务数" + +msgid "Parallel Writes" +msgstr "并行写入数" + +msgid "Paste token here…" +msgstr "在此粘贴令牌…" + +msgid "Perceptual Noise Substitution (PNS)" +msgstr "感知噪声替代 (PNS)" + +msgid "Performance" +msgstr "性能" + +msgid "Photos" +msgstr "照片" + +msgid "Platform default" +msgstr "平台默认" + +msgid "Playlists" +msgstr "播放列表" + +msgid "PODCAST PLAYLISTS" +msgstr "播客播放列表" + +msgid "Please report this issue on GitHub." +msgstr "请在 GitHub 上报告这个问题。" + +msgid "Podcasts" +msgstr "播客" + +msgid "Prefer Lossy Encoding" +msgstr "优先有损编码" + +msgid "Press Select to choose your iPod" +msgstr "点击“选择”来选择你的 iPod" + +msgid "Preview and apply iPod-friendly tag fixes across the whole library." +msgstr "预览并在整个资料库中应用适合 iPod 的标签修复。" + +msgid "Rating Conflict Strategy" +msgstr "评分冲突策略" + +msgid "REGULAR PLAYLISTS" +msgstr "普通播放列表" + +msgid "Reading library and device settings." +msgstr "正在读取资料库和设备设置。" + +msgid "Reencode Existing Lossy Files" +msgstr "重新编码已有有损文件" + +msgid "Report a Bug" +msgstr "报告问题" + +msgid "Rescan" +msgstr "重新扫描" + +msgid "Reset" +msgstr "重置" + +msgid "Reset Device Settings" +msgstr "重置设备设置" + +msgid "Reset to auto-detect" +msgstr "恢复自动检测" + +msgid "Size" +msgstr "大小" + +msgid "Sort" +msgstr "排序" + +msgid "SMART PLAYLISTS" +msgstr "智能播放列表" + +msgid "INTERNAL BROWSE CATEGORIES" +msgstr "内部浏览分类" + +msgid "Reset to default" +msgstr "恢复默认" + +msgid "Rotate Tall Photos on Device" +msgstr "在设备上旋转竖图" + +msgid "Round album art corners in grid cards and track lists. This only changes how artwork is drawn in iOpenPod and does not modify anything written to your iPod." +msgstr "在网格卡片和曲目列表中为专辑封面使用圆角。这只影响 iOpenPod 内的显示,不会修改写入 iPod 的内容。" + +msgid "Rounded Artwork" +msgstr "圆角封面" + +msgid "Safely eject the iPod from your system" +msgstr "从系统安全弹出 iPod" + +msgid "Save failed" +msgstr "保存失败" + +msgid "Saved" +msgstr "已保存" + +msgid "Saving…" +msgstr "正在保存…" + +msgid "Scale text size across the interface for accessibility." +msgstr "缩放整个界面的文字大小以改善可访问性。" + +msgid "Scheme 1" +msgstr "方案 1" + +msgid "Scheme 2" +msgstr "方案 2" + +msgid "Scrobbling" +msgstr "播放记录" + +msgid "Select" +msgstr "选择" + +msgid "Select Device" +msgstr "选择设备" + +msgid "Select File" +msgstr "选择文件" + +msgid "Select Folder" +msgstr "选择文件夹" + +msgid "Select an iPod to continue" +msgstr "请选择一个 iPod 继续" + +msgid "Select an iPod to edit device settings" +msgstr "选择一个 iPod 以编辑设备设置" + +msgid "Settings" +msgstr "设置" + +msgid "Settings Folder" +msgstr "设置文件夹" + +msgid "Sharpen Artwork" +msgstr "锐化封面" + +msgid "Show album art thumbnails next to tracks in the list view." +msgstr "在列表视图的曲目旁显示专辑封面缩略图。" + +msgid "Smart Quality by Content Type" +msgstr "按内容类型智能选择质量" + +msgid "Songs" +msgstr "歌曲" + +msgid "Spoken Word" +msgstr "语音内容" + +msgid "Spoken Word Bitrate" +msgstr "语音内容码率" + +msgid "Status" +msgstr "状态" + +msgid "Storage" +msgstr "存储" + +msgid "Storage Paths" +msgstr "存储路径" + +msgid "Support iOpenPod" +msgstr "支持 iOpenPod" + +msgid "Sync" +msgstr "同步" + +msgid "Sync with PC" +msgstr "与电脑同步" + +msgid "TV Shows" +msgstr "电视节目" + +msgid "Technical Details" +msgstr "技术详情" + +msgid "Temporal Noise Shaping (TNS)" +msgstr "瞬态噪声整形 (TNS)" + +msgid "The selected folder could not be identified as an iPod." +msgstr "无法将所选文件夹识别为 iPod。" + +msgid "The selected folder does not appear to be a valid iPod root.\n" +"\n" +"Expected structure:\n" +" /iPod_Control/iTunes/\n" +"\n" +"Please select the root folder of your iPod." +msgstr "所选文件夹不像有效的 iPod 根目录。\n" +"\n" +"预期结构:\n" +" /iPod_Control/iTunes/\n" +"\n" +"请选择 iPod 的根文件夹。" + +msgid "Theme" +msgstr "主题" + +msgid "Track List Artwork" +msgstr "曲目列表封面" + +msgid "Tracks" +msgstr "曲目" + +msgid "Transcode Cache" +msgstr "转码缓存" + +msgid "Transcoding" +msgstr "转码" + +msgid "Use Global Settings" +msgstr "使用全局设置" + +msgid "VBR Quality Level" +msgstr "VBR 质量等级" + +msgid "Video" +msgstr "视频" + +msgid "Video Encode Speed" +msgstr "视频编码速度" + +msgid "Video Quality (CRF)" +msgstr "视频质量 (CRF)" + +msgid "Videos" +msgstr "视频" + +msgid "Waiting for browser approval..." +msgstr "正在等待浏览器授权..." + +msgid "Write Back to PC" +msgstr "写回电脑" + +msgid "Yes" +msgstr "是" + +msgid "fpcalc (Chromaprint)" +msgstr "fpcalc (Chromaprint)" + +msgid "fpcalc Path Override" +msgstr "fpcalc 路径覆盖" + +msgid "iOpenPod Error" +msgstr "iOpenPod 错误" + +msgid "iOpenPod is and always will be completely free and open source. If you like it and would like to support me, it is so very appreciated." +msgstr "iOpenPod 现在和将来都会是完全免费且开源的软件。如果你喜欢它并愿意支持,我会非常感谢。" + +msgid "iPod Not Writable" +msgstr "iPod 不可写" + +msgid "libfdk_aac Afterburner" +msgstr "libfdk_aac Afterburner" + +msgid "{free:.1f} GB free of {total:.1f} GB" +msgstr "可用 {free:.1f} GB,共 {total:.1f} GB" + +msgid "(all columns shown)" +msgstr "(已显示所有列)" + +msgid "{count:,} audiobook" +msgstr "{count:,} 本有声书" + +msgid "{count:,} audiobooks" +msgstr "{count:,} 本有声书" + +msgid "{count:,} episode" +msgstr "{count:,} 集" + +msgid "{count:,} episodes" +msgstr "{count:,} 集" + +msgid "{count:,} song" +msgstr "{count:,} 首歌曲" + +msgid "{count:,} songs" +msgstr "{count:,} 首歌曲" + +msgid "{count:,} track" +msgstr "{count:,} 首曲目" + +msgid "{count:,} tracks" +msgstr "{count:,} 首曲目" + +msgid "{count:,} video" +msgstr "{count:,} 个视频" + +msgid "{count:,} videos" +msgstr "{count:,} 个视频" + +msgid "{shown:,} of {total:,} audiobook" +msgstr "显示 {shown:,} / {total:,} 本有声书" + +msgid "{shown:,} of {total:,} audiobooks" +msgstr "显示 {shown:,} / {total:,} 本有声书" + +msgid "{shown:,} of {total:,} episode" +msgstr "显示 {shown:,} / {total:,} 集" + +msgid "{shown:,} of {total:,} episodes" +msgstr "显示 {shown:,} / {total:,} 集" + +msgid "{shown:,} of {total:,} song" +msgstr "显示 {shown:,} / {total:,} 首歌曲" + +msgid "{shown:,} of {total:,} songs" +msgstr "显示 {shown:,} / {total:,} 首歌曲" + +msgid "{shown:,} of {total:,} track" +msgstr "显示 {shown:,} / {total:,} 首曲目" + +msgid "{shown:,} of {total:,} tracks" +msgstr "显示 {shown:,} / {total:,} 首曲目" + +msgid "{shown:,} of {total:,} video" +msgstr "显示 {shown:,} / {total:,} 个视频" + +msgid "{shown:,} of {total:,} videos" +msgstr "显示 {shown:,} / {total:,} 个视频" + +msgid "{gb:.2f} GB used of {max_gb:.0f} GB · {count:,} file" +msgstr "已用 {gb:.2f} GB,共 {max_gb:.0f} GB · {count:,} 个文件" + +msgid "{gb:.2f} GB used of {max_gb:.0f} GB · {count:,} files" +msgstr "已用 {gb:.2f} GB,共 {max_gb:.0f} GB · {count:,} 个文件" + +msgid "{gb:.2f} GB · {count:,} file" +msgstr "{gb:.2f} GB · {count:,} 个文件" + +msgid "{gb:.2f} GB · {count:,} files" +msgstr "{gb:.2f} GB · {count:,} 个文件" + +msgid "Add Column" +msgstr "添加列" + +msgid "Album Artist" +msgstr "专辑艺人" + +msgid "Album ID" +msgstr "专辑 ID" + +msgid "Art Count" +msgstr "封面数量" + +msgid "Art Formats:" +msgstr "封面格式:" + +msgid "Artist Ref" +msgstr "艺人引用" + +msgid "Artwork" +msgstr "封面" + +msgid "Artwork Ref" +msgstr "封面引用" + +msgid "Audio:" +msgstr "音频:" + +msgid "Audio Quality" +msgstr "音频质量" + +msgid "Bitrate" +msgstr "码率" + +msgid "BPM" +msgstr "BPM" + +msgid "Bus/Format:" +msgstr "总线/格式:" + +msgid "Cache Location" +msgstr "缓存位置" + +msgid "Capabilities" +msgstr "能力" + +msgid "Category" +msgstr "分类" + +msgid "Chapter Img:" +msgstr "章节图像:" + +msgid "Chapter Titles" +msgstr "章节标题" + +msgid "Chapters" +msgstr "章节" + +msgid "Cleared — {count:,} file removed" +msgstr "已清除 - 移除 {count:,} 个文件" + +msgid "Cleared — {count:,} files removed" +msgstr "已清除 - 移除 {count:,} 个文件" + +msgid "Comment" +msgstr "备注" + +msgid "Compilation" +msgstr "合辑" + +msgid "Composer" +msgstr "作曲者" + +msgid "Composer ID" +msgstr "作曲者 ID" + +msgid "Conflicts:" +msgstr "冲突:" + +msgid "Convert to a single chaptered track" +msgstr "转换为单个带章节曲目" + +msgid "Core Metadata" +msgstr "核心元数据" + +msgid "Could not prepare artwork:\n\n{error}" +msgstr "无法准备封面:\n\n{error}" + +msgid "Could not stage artwork update:\n\n{error}" +msgstr "无法暂存封面更新:\n\n{error}" + +msgid "Custom directory to store iOpenPod settings. Useful for portable setups or backups." +msgstr "用于存储 iOpenPod 设置的自定义目录,适合便携配置或备份。" + +msgid "Date Added" +msgstr "添加日期" + +msgid "Date Modified" +msgstr "修改日期" + +msgid "Dates" +msgstr "日期" + +msgid "Description" +msgstr "描述" + +msgid "Device DB:" +msgstr "设备数据库:" + +msgid "Disc #" +msgstr "光盘编号" + +msgid "Disc Total" +msgstr "光盘总数" + +msgid "Disk Size:" +msgstr "磁盘大小:" + +msgid "Encoder" +msgstr "编码器" + +msgid "Enclosure URL" +msgstr "媒体文件 URL" + +msgid "Episode #" +msgstr "剧集编号" + +msgid "Episode ID" +msgstr "剧集 ID" + +msgid "Equalizer" +msgstr "均衡器" + +msgid "Error clearing cache: {error}" +msgstr "清除缓存失败:{error}" + +msgid "File Format" +msgstr "文件格式" + +msgid "Flags" +msgstr "标记" + +msgid "Free Space:" +msgstr "可用空间:" + +msgid "Gapless" +msgstr "无缝播放" + +msgid "Gapless Album" +msgstr "无缝专辑" + +msgid "Gapless Payload" +msgstr "无缝负载" + +msgid "Grouping" +msgstr "分组" + +msgid "Has Lyrics" +msgstr "有歌词" + +msgid "Hash Scheme:" +msgstr "哈希方案:" + +msgid "Hide \"{column}\"" +msgstr "隐藏“{column}”" + +msgid "Identity" +msgstr "身份" + +msgid "Identifiers" +msgstr "标识符" + +msgid "Keywords" +msgstr "关键词" + +msgid "Largest" +msgstr "最大" + +msgid "Last Played" +msgstr "上次播放" + +msgid "Last Skipped" +msgstr "上次跳过" + +msgid "Locations" +msgstr "位置" + +msgid "Locale" +msgstr "区域" + +msgid "Location" +msgstr "位置" + +msgid "Log Location" +msgstr "日志位置" + +msgid "Lyrics" +msgstr "歌词" + +msgid "Max Cache Size" +msgstr "最大缓存大小" + +msgid "Max File:" +msgstr "最大文件:" + +msgid "Max Transfer:" +msgstr "最大传输:" + +msgid "Media Type" +msgstr "媒体类型" + +msgid "Minimize" +msgstr "最小化" + +msgid "Model #:" +msgstr "型号:" + +msgid "Most Albums" +msgstr "专辑最多" + +msgid "Most Artists" +msgstr "艺人最多" + +msgid "Most Plays" +msgstr "播放最多" + +msgid "Most Skipped" +msgstr "跳过最多" + +msgid "Most Tracks" +msgstr "曲目最多" + +msgid "Network" +msgstr "电视网" + +msgid "Oldest cached files are automatically removed (LRU) to stay within this limit. Set to Unlimited if storage is not a concern." +msgstr "最旧的缓存文件会按 LRU 自动移除,以保持在此限制内。如果不担心存储空间,可设为无限制。" + +msgid "Open ↗" +msgstr "打开 ↗" + +msgid "Other" +msgstr "其他" + +msgid "Photo Formats:" +msgstr "照片格式:" + +msgid "Playback && Stats" +msgstr "播放与统计" + +msgid "Played" +msgstr "已播放" + +msgid "Plays" +msgstr "播放次数" + +msgid "Plays (iPod)" +msgstr "播放次数 (iPod)" + +msgid "Podcast" +msgstr "播客" + +msgid "Podcasts:" +msgstr "播客:" + +msgid "Post-gap" +msgstr "后间隙" + +msgid "Pre-gap" +msgstr "前间隙" + +msgid "Product:" +msgstr "产品:" + +msgid "Rating" +msgstr "评分" + +msgid "Release Date" +msgstr "发行日期" + +msgid "Remember Pos." +msgstr "记忆位置" + +msgid "Resize All Columns to Fit" +msgstr "调整所有列以适应内容" + +msgid "Resize Column to Fit" +msgstr "调整列以适应内容" + +msgid "RSS URL" +msgstr "RSS URL" + +msgid "Sample Count" +msgstr "采样数" + +msgid "Sample Rate" +msgstr "采样率" + +msgid "Search…" +msgstr "搜索…" + +msgid "Season" +msgstr "季" + +msgid "Select an Album" +msgstr "选择专辑" + +msgid "Select an Artist" +msgstr "选择艺人" + +msgid "Select a Genre" +msgstr "选择流派" + +msgid "Serial:" +msgstr "序列号:" + +msgid "Settings Location" +msgstr "设置位置" + +msgid "Shadow DB:" +msgstr "影子数据库:" + +msgid "Show" +msgstr "节目" + +msgid "Skip Shuffle" +msgstr "随机播放时跳过" + +msgid "Skips" +msgstr "跳过次数" + +msgid "Sort: {label} ▾" +msgstr "排序:{label} ▾" + +msgid "Sort Album" +msgstr "排序专辑" + +msgid "Sort Album Artist" +msgstr "排序专辑艺人" + +msgid "Sort Artist" +msgstr "排序艺人" + +msgid "Sort Composer" +msgstr "排序作曲者" + +msgid "Sort Overrides" +msgstr "排序覆盖" + +msgid "Sort Show" +msgstr "排序节目" + +msgid "Sort Title" +msgstr "排序标题" + +msgid "Sparse Art:" +msgstr "稀疏封面:" + +msgid "Start Time" +msgstr "开始时间" + +msgid "Stop Time" +msgstr "结束时间" + +msgid "Subtitle" +msgstr "字幕" + +msgid "Technical Details" +msgstr "技术详情" + +msgid "Time" +msgstr "时长" + +msgid "Title" +msgstr "标题" + +msgid "Track #" +msgstr "曲目编号" + +msgid "Track ID" +msgstr "曲目 ID" + +msgid "Track Keywords" +msgstr "曲目关键词" + +msgid "Track Total" +msgstr "曲目总数" + +msgid "Unavailable ({error})" +msgstr "不可用({error})" + +msgid "Unify Artwork" +msgstr "统一封面" + +msgid "Updater ID:" +msgstr "更新器 ID:" + +msgid "USB / SCSI" +msgstr "USB / SCSI" + +msgid "USB Parent:" +msgstr "USB 父级:" + +msgid "USB PID:" +msgstr "USB PID:" + +msgid "USB Serial:" +msgstr "USB 序列号:" + +msgid "USB VID:" +msgstr "USB VID:" + +msgid "Voice Memos:" +msgstr "语音备忘录:" + +msgid "Volume Adj." +msgstr "音量调整" + +msgid "Where iOpenPod writes log files and crash reports. Takes effect on next launch." +msgstr "iOpenPod 写入日志文件和崩溃报告的位置。下次启动后生效。" + +msgid "Where transcoded files are cached to avoid re-encoding on future syncs." +msgstr "转码文件的缓存位置,用于避免后续同步时重复编码。" + +msgid "Year" +msgstr "年份" + +msgid "{value}\nSource: {source}" +msgstr "{value}\n来源:{source}" + +msgid "(mixed selection)" +msgstr "(选择内容不一致)" + +msgid "Add to Playlist" +msgstr "添加到播放列表" + +msgid "Checked" +msgstr "已勾选" + +msgid "Content Advisory" +msgstr "内容分级" + +msgid "Content Advisory: Clean" +msgstr "内容分级:洁净版" + +msgid "Content Advisory: Explicit" +msgstr "内容分级:限制级" + +msgid "Convert to Podcast" +msgstr "转换为播客" + +msgid "Copy as File(s)" +msgstr "复制为文件" + +msgid "Copy as Text" +msgstr "复制为文本" + +msgid "Downloading update…" +msgstr "正在下载更新…" + +msgid "Edit ({count})" +msgstr "编辑({count})" + +msgid "Explicit" +msgstr "限制级" + +msgid "ffprobe missing" +msgstr "缺少 ffprobe" + +msgid "iOpenPod Update" +msgstr "iOpenPod 更新" + +msgid "Invalid token" +msgstr "令牌无效" + +msgid "Move Down" +msgstr "下移" + +msgid "Move Up" +msgstr "上移" + +msgid "New Playlist" +msgstr "新建播放列表" + +msgid "No Binary Available" +msgstr "没有可用的二进制文件" + +msgid "No pre-built binary was found for your platform.\n\nVisit {release_page} to download manually." +msgstr "没有找到适用于你平台的预构建二进制文件。\n\n请访问 {release_page} 手动下载。" + +msgid "No Rating" +msgstr "无评分" + +msgid "None (Unset)" +msgstr "无(未设置)" + +msgid "Part of a compilation album" +msgstr "合辑专辑的一部分" + +msgid "Preparing {count} file…" +msgstr "正在准备 {count} 个文件…" + +msgid "Preparing {count} files…" +msgstr "正在准备 {count} 个文件…" + +msgid "Remove {count} Track from iPod" +msgstr "从 iPod 移除 {count} 首曲目" + +msgid "Remove {count} Tracks from iPod" +msgstr "从 iPod 移除 {count} 首曲目" + +msgid "Remove {count} Track from Playlist" +msgstr "从播放列表移除 {count} 首曲目" + +msgid "Remove {count} Tracks from Playlist" +msgstr "从播放列表移除 {count} 首曲目" + +msgid "Reset Columns" +msgstr "重置列" + +msgid "Skip When Shuffling" +msgstr "随机播放时跳过" + +msgid "Skip this track in shuffle mode" +msgstr "随机播放时跳过此曲目" + +msgid "Split chapters into individual tracks" +msgstr "将章节拆分为单独曲目" + +msgid "Starting drag…" +msgstr "正在开始拖拽…" + +msgid "Untitled" +msgstr "未命名" + +msgid "Up to Date" +msgstr "已是最新" + +msgid "Update Check Failed" +msgstr "更新检查失败" + +msgid "Validating…" +msgstr "正在验证…" + +msgid "Volume Adjustment" +msgstr "音量调整" + +msgid "You are running the latest version (v{version})." +msgstr "你正在运行最新版本 (v{version})。" + +msgid "Clean" +msgstr "洁净版" + +msgid "Database" +msgstr "数据库" + +msgid "Edit" +msgstr "编辑" + +msgid "Maximize" +msgstr "最大化" + +msgid "{count} chapter" +msgstr "{count} 个章节" + +msgid "{count} chapters" +msgstr "{count} 个章节" + + +#: GUI/app.py:2312 +msgid "" +"\n" +"\n" +"If you discard, the copied files will be cleaned up automatically the next time you sync." +msgstr "\n\n如果选择丢弃,已复制的文件会在下次同步时自动清理。" + +#: app_core/jobs.py:70 +msgid " and " +msgstr " 和 " + +#: GUI/app.py:1439 GUI/app.py:1433 +msgid "Album Conversion" +msgstr "专辑转换" + +#: GUI/app.py:1594 +msgid "Chapter Split" +msgstr "章节拆分" + +#: GUI/app.py:1440 +msgid "Choose an album with at least two tracks." +msgstr "请选择至少包含两首曲目的专辑。" + +#: GUI/app.py:781 +msgid "Could Not Load iPod" +msgstr "无法加载 iPod" + +#: GUI/app.py:1379 +msgid "" +"Could not download sync tools:\n" +"\n" +"{error}" +msgstr "无法下载同步工具:\n\n{error}" + +#: GUI/app.py:1102 +msgid "" +"Could not save quick changes to iPod:\n" +"{error}\n" +"\n" +"iOpenPod is reloading the device view from the iPod." +msgstr "无法将快速更改保存到 iPod:\n{error}\n\niOpenPod 正在从 iPod 重新加载设备视图。" + +#: GUI/app.py:955 app_core/jobs.py:1940 app_core/jobs.py:1989 app_core/jobs.py:2265 +msgid "Database write failed." +msgstr "数据库写入失败。" + +#: GUI/app.py:958 +msgid "Device name updated successfully" +msgstr "设备名称已更新" + +#: GUI/app.py:2317 +msgid "Discard" +msgstr "丢弃" + +#: GUI/app.py:1378 +msgid "Download Failed" +msgstr "下载失败" + +#: GUI/app.py:2625 +msgid "Downloading" +msgstr "正在下载" + +#: GUI/app.py:2638 +msgid "Downloading Tools…" +msgstr "正在下载工具…" + +#: GUI/app.py:1075 +msgid "Eject Failed" +msgstr "弹出失败" + +#: app_core/jobs.py:84 +msgid "" +"FFmpeg and ffprobe are required for transcoding and media probing.\n" +"Install from: https://ffmpeg.org" +msgstr "转码和媒体探测需要 FFmpeg 与 ffprobe。\n请从这里安装:https://ffmpeg.org" + +#: GUI/app.py:1076 +msgid "" +"Failed to eject the iPod:\n" +"{error}" +msgstr "无法弹出 iPod:\n{error}" + +#: GUI/app.py:966 +msgid "" +"Failed to rename iPod:\n" +"{error}" +msgstr "无法重命名 iPod:\n{error}" + +#: GUI/app.py:1423 GUI/app.py:1582 GUI/app.py:1184 +msgid "Library Loading" +msgstr "资料库正在加载" + +#: GUI/app.py:791 +msgid "Load an iPod library before running tag normalization." +msgstr "请先加载 iPod 资料库,再运行标签规范化。" + +#: GUI/app.py:2525 +msgid "Missing Tools" +msgstr "缺少工具" + +#: app_core/jobs.py:1922 app_core/jobs.py:1971 +msgid "No iPod connected." +msgstr "未连接 iPod。" + +#: app_core/jobs.py:1926 app_core/jobs.py:1975 +msgid "No iPod database loaded." +msgstr "未加载 iPod 数据库。" + +#: GUI/app.py:2087 +msgid "No iPod device selected." +msgstr "未选择 iPod 设备。" + +#: GUI/app.py:824 +msgid "No iPod-specific metadata fixes were found for this library." +msgstr "没有在这个资料库中发现需要针对 iPod 修正的元数据。" + +#: GUI/app.py:800 +msgid "No tracks were found in this iPod library." +msgstr "这个 iPod 资料库中没有找到曲目。" + +#: GUI/app.py:790 GUI/app.py:799 GUI/app.py:823 +msgid "Normalize iPod Tags" +msgstr "规范化 iPod 标签" + +#: GUI/app.py:2184 +msgid "Not Enough Space" +msgstr "空间不足" + +#: GUI/app.py:2575 +msgid "Not Now" +msgstr "暂不" + +#: GUI/app.py:2603 +msgid "OK" +msgstr "确定" + +#: GUI/app.py:1418 GUI/app.py:1575 +msgid "Please select an iPod device first." +msgstr "请先选择 iPod 设备。" + +#: GUI/app.py:1412 +msgid "Please wait for the current sync to finish before converting an album." +msgstr "请等待当前同步完成后再转换专辑。" + +#: GUI/app.py:1030 +msgid "Please wait for the current sync to finish before ejecting." +msgstr "请等待当前同步完成后再弹出。" + +#: GUI/app.py:1569 +msgid "Please wait for the current sync to finish before splitting chapters." +msgstr "请等待当前同步完成后再拆分章节。" + +#: GUI/app.py:1423 GUI/app.py:1583 GUI/app.py:1185 +msgid "Please wait for the iPod library to finish loading." +msgstr "请等待 iPod 资料库加载完成。" + +#: GUI/app.py:2644 +msgid "Preparing download…" +msgstr "正在准备下载…" + +#: GUI/app.py:1174 +msgid "Quick Changes Still Saving" +msgstr "快速更改仍在保存" + +#: GUI/app.py:2421 +msgid "Reading dropped files..." +msgstr "正在读取拖入的文件..." + +#: GUI/app.py:965 +msgid "Rename Failed" +msgstr "重命名失败" + +#: GUI/app.py:1101 +msgid "Save Failed" +msgstr "保存失败" + +#: GUI/app.py:979 +msgid "Save In Progress" +msgstr "正在保存" + +#: GUI/app.py:2316 +msgid "Save Partial Database" +msgstr "保存部分数据库" + +#: GUI/app.py:2301 +msgid "Save Partial Sync?" +msgstr "保存部分同步结果?" + +#: GUI/app.py:1008 +msgid "Still Reading iPod" +msgstr "仍在读取 iPod" + +#: GUI/app.py:2280 +msgid "Sync Error" +msgstr "同步错误" + +#: GUI/app.py:2204 +msgid "Sync Failed" +msgstr "同步失败" + +#: GUI/app.py:1029 +msgid "Sync In Progress" +msgstr "正在同步" + +#: GUI/app.py:1411 GUI/app.py:1568 +msgid "Sync Running" +msgstr "同步正在运行" + +#: GUI/app.py:2189 +msgid "Sync Until Full" +msgstr "同步直到写满" + +#: GUI/app.py:167 +msgid "Sync failed before making changes." +msgstr "同步在执行更改前失败。" + +#: GUI/app.py:2185 +msgid "The selected sync is larger than the iPod's free space." +msgstr "所选同步内容超过了 iPod 的可用空间。" + +#: GUI/app.py:1393 +msgid "This iPod does not support podcasts." +msgstr "这台 iPod 不支持播客。" + +#: GUI/app.py:2166 +msgid "" +"This sync is estimated to need more space than is available on the iPod.\n" +"\n" +"Available: {available}\n" +"Estimated needed: {required}\n" +"Estimated shortfall: {shortage}\n" +"\n" +"Sync Until Full will copy files in order until the next file would leave less than {reserve} free, then save the database with the items that actually synced." +msgstr "本次同步预计需要的空间超过 iPod 当前可用空间。\n\n可用空间:{available}\n预计需要:{required}\n预计缺口:{shortage}\n\n“同步直到写满”会按顺序复制文件,直到下一个文件会导致剩余空间低于 {reserve},然后只保存实际同步成功的项目到数据库。" + +#: GUI/app.py:746 GUI/app.py:748 +msgid "Unknown iPod" +msgstr "未知 iPod" + +#: GUI/app.py:1392 +msgid "Unsupported iPod" +msgstr "不支持的 iPod" + +#: GUI/app.py:2309 +msgid "Would you like to save these tracks to your iPod's database?" +msgstr "要将这些曲目保存到 iPod 数据库吗?" + +#: app_core/jobs.py:89 +msgid "" +"You can also set custom paths in\n" +"Settings -> External Tools." +msgstr "你也可以在\n设置 -> 外部工具\n中设置自定义路径。" + +#: app_core/jobs.py:77 +msgid "" +"fpcalc is required for sync.\n" +"Install from: https://acoustid.org/chromaprint" +msgstr "同步需要 fpcalc。\n请从这里安装:https://acoustid.org/chromaprint" + +#: GUI/app.py:2555 +msgid "" +"iOpenPod can download these automatically (~80 MB).\n" +"Download now?" +msgstr "iOpenPod 可以自动下载这些工具(约 80 MB)。\n现在下载吗?" + +#: GUI/app.py:186 +msgid "" +"iOpenPod could not load this iPod library.\n" +"\n" +"{error}" +msgstr "iOpenPod 无法加载这个 iPod 资料库。\n\n{error}" + +#: GUI/app.py:190 +msgid "" +"iOpenPod could not read this iPod cleanly.\n" +"\n" +"Mount path: {mount}\n" +"System error: {error}\n" +"\n" +"On Linux, this usually means the iPod mount is not accessible, the FAT filesystem is dirty, or the current user does not have permission to the mount.\n" +"\n" +"Try reconnecting the iPod. If it still fails, try remounting it read-write:\n" +" sudo mount -o remount,rw {quoted_mount}\n" +"\n" +"If the filesystem is dirty, unmount it before repairing it:\n" +" sudo umount {quoted_mount}\n" +" sudo fsck.vfat -a /dev/sdXN\n" +"\n" +"Replace /dev/sdXN with the iPod partition. Do not run fsck while the iPod is mounted." +msgstr "iOpenPod 无法干净地读取这台 iPod。\n\n挂载路径:{mount}\n系统错误:{error}\n\n在 Linux 上,这通常表示 iPod 挂载点不可访问、FAT 文件系统状态不干净,或当前用户没有挂载点权限。\n\n请先尝试重新连接 iPod。如果仍然失败,可以尝试以读写方式重新挂载:\n sudo mount -o remount,rw {quoted_mount}\n\n如果文件系统状态不干净,请先卸载再修复:\n sudo umount {quoted_mount}\n sudo fsck.vfat -a /dev/sdXN\n\n请将 /dev/sdXN 替换为 iPod 分区。不要在 iPod 已挂载时运行 fsck。" + +#: GUI/app.py:1009 +msgid "iOpenPod is still finishing background reads from the iPod. Try ejecting again in a moment." +msgstr "iOpenPod 仍在完成对 iPod 的后台读取。请稍后再尝试弹出。" + +#: GUI/app.py:1175 +msgid "iOpenPod is still saving pending quick changes. Please wait for {label} to finish before starting a full sync." +msgstr "iOpenPod 仍在保存待处理的快速更改。请等待 {label} 完成后再开始完整同步。" + +#: GUI/app.py:980 +msgid "iOpenPod is still saving {label} to the iPod. Try ejecting again when the save finishes." +msgstr "iOpenPod 仍在将 {label} 保存到 iPod。请在保存完成后再次尝试弹出。" + +#: GUI/app.py:1050 +msgid "iPod Ejected" +msgstr "iPod 已弹出" + +#: GUI/app.py:958 +msgid "iPod Renamed" +msgstr "iPod 已重命名" + +#: GUI/app.py:1171 +msgid "quick changes" +msgstr "快速更改" + +#: GUI/app.py:2290 +msgid "track" +msgstr "首曲目" + +#: GUI/app.py:2290 +msgid "tracks" +msgstr "首曲目" + +#: GUI/app.py:2293 +msgid "{count} more track was not copied." +msgstr "还有 {count} 首曲目未复制。" + +#: GUI/app.py:2295 +msgid "{count} more tracks were not copied." +msgstr "还有 {count} 首曲目未复制。" + +#: GUI/app.py:2304 +msgid "{count} {tracks_word} were successfully copied to your iPod before the sync was cancelled." +msgstr "同步取消前,已有 {count} {tracks_word}成功复制到 iPod。" + +#: GUI/app.py:2544 +msgid "{tools} Not Found" +msgstr "未找到 {tools}" + + +#: GUI/widgets/photoBrowser.py:1694 +msgid "Add Photo to Album" +msgstr "添加照片到相簿" + +#: GUI/widgets/photoBrowser.py:1246 GUI/widgets/photoBrowser.py:480 +msgid "Add to Album" +msgstr "添加到相簿" + +#: GUI/widgets/photoBrowser.py:1676 +msgid "Album name:" +msgstr "相簿名:" + +#: GUI/widgets/photoBrowser.py:1695 +msgid "Album:" +msgstr "相簿:" + +#: GUI/widgets/photoBrowser.py:814 +msgid "All Photos" +msgstr "所有照片" + +#: GUI/widgets/playlistBrowser.py:1012 +msgid "Choose a playlist to inspect its tracks and database metadata" +msgstr "选择一个播放列表以查看曲目和数据库元数据" + +#: GUI/widgets/photoBrowser.py:1689 +msgid "Create another album first, or choose a photo that is not already in every album." +msgstr "请先创建另一个相簿,或选择一张尚未加入所有相簿的照片。" + +#: GUI/widgets/playlistBrowser.py:437 +msgid "Delete" +msgstr "删除" + +#: GUI/widgets/photoBrowser.py:1724 GUI/widgets/photoBrowser.py:1742 +msgid "Delete '{name}' from the iPod now?" +msgstr "现在从 iPod 删除“{name}”吗?" + +#: GUI/widgets/photoBrowser.py:1323 GUI/widgets/photoBrowser.py:1723 +msgid "Delete Album" +msgstr "删除相簿" + +#: GUI/widgets/photoBrowser.py:1267 GUI/widgets/photoBrowser.py:1741 GUI/widgets/photoBrowser.py:482 +msgid "Delete Photo" +msgstr "删除照片" + +#: GUI/widgets/playlistBrowser.py:561 +msgid "Details" +msgstr "详情" + +#: GUI/widgets/playlistBrowser.py:451 GUI/widgets/playlistBrowser.py:1854 GUI/widgets/playlistBrowser.py:1879 +msgid "Evaluate Now" +msgstr "立即计算" + +#: GUI/widgets/photoBrowser.py:479 GUI/widgets/playlistBrowser.py:473 +msgid "Export" +msgstr "导出" + +#: GUI/widgets/photoBrowser.py:1660 +msgid "Export Album" +msgstr "导出相簿" + +#: GUI/widgets/photoBrowser.py:1296 +msgid "Export Album..." +msgstr "导出相簿..." + +#: GUI/widgets/photoBrowser.py:1660 +msgid "Export All Photos" +msgstr "导出所有照片" + +#: GUI/widgets/photoBrowser.py:1296 +msgid "Export All Photos..." +msgstr "导出所有照片..." + +#: GUI/widgets/photoBrowser.py:1634 +msgid "Export Photo" +msgstr "导出照片" + +#: GUI/widgets/photoBrowser.py:1238 +msgid "Export Photo..." +msgstr "导出照片..." + +#: GUI/widgets/playlistBrowser.py:1338 GUI/widgets/playlistBrowser.py:1974 GUI/widgets/playlistBrowser.py:1603 +msgid "Import Playlist" +msgstr "导入播放列表" + +#: GUI/widgets/playlistBrowser.py:1401 +msgid "Importing Playlist…" +msgstr "正在导入播放列表…" + +#: GUI/widgets/playlistBrowser.py:1603 +msgid "Importing…" +msgstr "正在导入…" + +#: GUI/widgets/photoBrowser.py:423 GUI/widgets/photoBrowser.py:1305 GUI/widgets/photoBrowser.py:1676 +msgid "New Album" +msgstr "新建相簿" + +#: GUI/widgets/photoBrowser.py:1710 +msgid "New album name:" +msgstr "新相簿名:" + +#: GUI/widgets/photoBrowser.py:1688 +msgid "No Available Albums" +msgstr "没有可用相簿" + +#: GUI/widgets/photoBrowser.py:1574 GUI/widgets/photoBrowser.py:1650 +msgid "No Photos" +msgstr "没有照片" + +#: GUI/widgets/photoBrowser.py:1454 GUI/widgets/photoBrowser.py:1581 GUI/widgets/photoBrowser.py:1625 GUI/widgets/photoBrowser.py:1655 +msgid "No iPod Connected" +msgstr "未连接 iPod" + +#: GUI/widgets/photoBrowser.py:474 +msgid "No photo selected" +msgstr "未选择照片" + +#: GUI/widgets/playlistBrowser.py:1177 +msgid "No playlists on this iPod" +msgstr "此 iPod 上没有播放列表" + +#: GUI/widgets/playlistBrowser.py:1986 +msgid "Parsing playlist…" +msgstr "正在解析播放列表…" + +#: GUI/widgets/photoBrowser.py:481 +msgid "Remove from Album" +msgstr "从相簿移除" + +#: GUI/widgets/photoBrowser.py:1256 +msgid "Remove from Current Album" +msgstr "从当前相簿移除" + +#: GUI/widgets/photoBrowser.py:1710 GUI/widgets/photoBrowser.py:1316 +msgid "Rename Album" +msgstr "重命名相簿" + +#: GUI/widgets/playlistBrowser.py:528 +msgid "Rules" +msgstr "规则" + +#: GUI/widgets/photoBrowser.py:475 +msgid "Select a photo to inspect its preview and album details." +msgstr "选择照片以查看预览和相簿详情。" + +#: GUI/widgets/playlistBrowser.py:384 GUI/widgets/playlistBrowser.py:1007 +msgid "Select a playlist" +msgstr "选择播放列表" + +#: GUI/widgets/photoBrowser.py:1455 +msgid "Select an iPod before editing device photos." +msgstr "编辑设备照片前,请先选择一台 iPod。" + +#: GUI/widgets/photoBrowser.py:1582 GUI/widgets/photoBrowser.py:1626 GUI/widgets/photoBrowser.py:1656 +msgid "Select an iPod before exporting device photos." +msgstr "导出设备照片前,请先选择一台 iPod。" + +#: GUI/widgets/photoBrowser.py:1574 GUI/widgets/photoBrowser.py:1650 +msgid "There are no photos to export." +msgstr "没有可导出的照片。" + +#: GUI/widgets/playlistBrowser.py:1838 +msgid "Writing…" +msgstr "正在写入…" + + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "1" +msgstr "1" + +#: GUI/widgets/settingsPage.py:1950 +msgid "1 GB" +msgstr "1 GB" + +#: GUI/widgets/settingsPage.py:2008 GUI/widgets/settingsPage.py:2008 +msgid "10" +msgstr "10" + +#: GUI/widgets/settingsPage.py:1950 +msgid "10 GB" +msgstr "10 GB" + +#: GUI/widgets/settingsPage.py:1515 GUI/widgets/settingsPage.py:1515 +msgid "100%" +msgstr "100%" + +#: GUI/widgets/settingsPage.py:1515 +msgid "110%" +msgstr "110%" + +#: GUI/widgets/settingsPage.py:1515 +msgid "125%" +msgstr "125%" + +#: GUI/widgets/settingsPage.py:1716 +msgid "128 kbps" +msgstr "128 kbps" + +#: GUI/widgets/settingsPage.py:1787 +msgid "15 kHz" +msgstr "15 kHz" + +#: GUI/widgets/settingsPage.py:1515 +msgid "150%" +msgstr "150%" + +#: GUI/widgets/settingsPage.py:1787 +msgid "16 kHz" +msgstr "16 kHz" + +#: GUI/widgets/settingsPage.py:1716 +msgid "160 kbps" +msgstr "160 kbps" + +#: GUI/widgets/settingsPage.py:1787 +msgid "17 kHz" +msgstr "17 kHz" + +#: GUI/widgets/settingsPage.py:1751 +msgid "18 (High)" +msgstr "18(高)" + +#: GUI/widgets/settingsPage.py:1787 +msgid "18 kHz" +msgstr "18 kHz" + +#: GUI/widgets/settingsPage.py:1787 +msgid "19 kHz" +msgstr "19 kHz" + +#: GUI/widgets/settingsPage.py:1716 GUI/widgets/settingsPage.py:1716 +msgid "192 kbps" +msgstr "192 kbps" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "2" +msgstr "2" + +#: GUI/widgets/settingsPage.py:1950 +msgid "2 GB" +msgstr "2 GB" + +#: GUI/widgets/settingsPage.py:2008 +msgid "20" +msgstr "20" + +#: GUI/widgets/settingsPage.py:1751 +msgid "20 (Good)" +msgstr "20(良好)" + +#: GUI/widgets/settingsPage.py:1950 +msgid "20 GB" +msgstr "20 GB" + +#: GUI/widgets/settingsPage.py:1787 +msgid "20 kHz" +msgstr "20 kHz" + +#: GUI/widgets/settingsPage.py:1716 +msgid "224 kbps" +msgstr "224 kbps" + +#: GUI/widgets/settingsPage.py:1751 GUI/widgets/settingsPage.py:1751 +msgid "23 (Balanced)" +msgstr "23(均衡)" + +#: GUI/widgets/settingsPage.py:1716 +msgid "256 kbps" +msgstr "256 kbps" + +#: GUI/widgets/settingsPage.py:1751 +msgid "26 (Low)" +msgstr "26(低)" + +#: GUI/widgets/settingsPage.py:1751 +msgid "28 (Very Low)" +msgstr "28(很低)" + +#: GUI/widgets/settingsPage.py:1728 +msgid "32 kbps" +msgstr "32 kbps" + +#: GUI/widgets/settingsPage.py:1716 +msgid "320 kbps" +msgstr "320 kbps" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 +msgid "4" +msgstr "4" + +#: GUI/widgets/settingsPage.py:1728 +msgid "48 kbps" +msgstr "48 kbps" + +#: GUI/widgets/settingsPage.py:2008 +msgid "5" +msgstr "5" + +#: GUI/widgets/settingsPage.py:1950 GUI/widgets/settingsPage.py:1950 +msgid "5 GB" +msgstr "5 GB" + +#: GUI/widgets/settingsPage.py:1950 +msgid "50 GB" +msgstr "50 GB" + +#: GUI/widgets/settingsPage.py:1619 +msgid "6" +msgstr "6" + +#: GUI/widgets/settingsPage.py:1728 GUI/widgets/settingsPage.py:1728 +msgid "64 kbps" +msgstr "64 kbps" + +#: GUI/widgets/settingsPage.py:1515 +msgid "75%" +msgstr "75%" + +#: GUI/widgets/settingsPage.py:1619 +msgid "8" +msgstr "8" + +#: GUI/widgets/settingsPage.py:1728 +msgid "80 kbps" +msgstr "80 kbps" + +#: GUI/widgets/settingsPage.py:1515 +msgid "90%" +msgstr "90%" + +#: GUI/widgets/settingsPage.py:1716 GUI/widgets/settingsPage.py:1728 +msgid "96 kbps" +msgstr "96 kbps" + +#: GUI/widgets/settingsPage.py:1780 +msgid "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC.When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" +msgstr "始终以 44.1 kHz(CD 采样率)输出音频。建议用于早期 iPod(1G-4G),它们可能无法很好处理 48 kHz ALAC。关闭时,采样率会降到 48 kHz,因为 iPod 只能解码 48 kHz 或更低。" + +#: GUI/widgets/settingsPage.py:1640 +msgid "Analyze loudness of files missing ReplayGain/iTunNORM tags using ffmpeg, then write the result back into your PC files and sync to iPod. Sound Check values are always synced to iPod regardless of this setting." +msgstr "使用 ffmpeg 分析缺少 ReplayGain/iTunNORM 标签的文件响度,然后将结果写回电脑音乐文件并同步到 iPod。无论此设置如何,音量均衡值都会同步到 iPod。" + +#: GUI/widgets/settingsPage.py:1702 +msgid "Audio quality preset for music tracks. High: 256 kbps / best VBR. Balanced: 192 kbps. Compact: 128 kbps / smaller VBR." +msgstr "音乐曲目的音频质量预设。高质量:256 kbps / 最佳 VBR。均衡:192 kbps。紧凑:128 kbps / 更小的 VBR。" + +#: GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1619 GUI/widgets/settingsPage.py:1627 GUI/widgets/settingsPage.py:1627 GUI/widgets/settingsPage.py:1688 GUI/widgets/settingsPage.py:1688 GUI/widgets/settingsPage.py:1787 GUI/widgets/settingsPage.py:1787 +msgid "Auto" +msgstr "自动" + +#: GUI/widgets/settingsPage.py:1902 +msgid "Automatically scrobble new iPod plays to connected services when you sync." +msgstr "同步时自动把新的 iPod 播放记录提交到已连接的服务。" + +#: GUI/widgets/settingsPage.py:1658 +msgid "Average" +msgstr "平均" + +#: GUI/widgets/settingsPage.py:2001 +msgid "Backup Before Sync" +msgstr "同步前备份" + +#: GUI/widgets/settingsPage.py:1995 +msgid "Backup Location" +msgstr "备份位置" + +#: GUI/widgets/settingsPage.py:1702 GUI/widgets/settingsPage.py:1702 +msgid "Balanced" +msgstr "均衡" + +#: GUI/widgets/settingsPage.py:1522 GUI/widgets/settingsPage.py:1522 +msgid "Blue (Default)" +msgstr "蓝色(默认)" + +#: GUI/widgets/settingsPage.py:1709 GUI/widgets/settingsPage.py:1709 +msgid "CBR" +msgstr "CBR" + +#: GUI/widgets/settingsPage.py:1709 +msgid "CBR uses a fixed target bitrate. VBR targets a quality level and lets the encoder choose bitrate per frame — typically better quality per byte." +msgstr "CBR 使用固定目标码率。VBR 以质量等级为目标,让编码器按帧选择码率,通常能在相同体积下获得更好质量。" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Frappé" +msgstr "Catppuccin Frappé" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Latte" +msgstr "Catppuccin Latte" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Macchiato" +msgstr "Catppuccin Macchiato" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Catppuccin Mocha" +msgstr "Catppuccin Mocha" + +#: GUI/widgets/settingsPage.py:2001 +msgid "Choose whether sync creates a full device backup automatically, asks each time, or skips pre-sync backups." +msgstr "选择同步前是自动创建完整设备备份、每次询问,还是跳过同步前备份。" + +#: GUI/widgets/settingsPage.py:1688 +msgid "Choose which lossy encoder to use.Auto chooses the best available." +msgstr "选择要使用的有损编码器。自动会选择可用的最佳编码器。" + +#: GUI/widgets/settingsPage.py:1702 +msgid "Compact" +msgstr "紧凑" + +#: GUI/widgets/settingsPage.py:1918 +msgid "Connect your Last.fm account to scrobble iPod plays. You will need to provide your Last.fm API Key and API Secret." +msgstr "连接 Last.fm 账号以提交 iPod 播放记录。你需要提供 Last.fm API Key 和 API Secret。" + +#: GUI/widgets/settingsPage.py:1908 +msgid "Connect your ListenBrainz account to scrobble iPod plays. Copy your user token from the link below." +msgstr "连接 ListenBrainz 账号以提交 iPod 播放记录。请从下方链接复制用户令牌。" + +#: GUI/widgets/settingsPage.py:1495 GUI/widgets/settingsPage.py:1495 +msgid "Dark" +msgstr "深色" + +#: GUI/widgets/settingsPage.py:1769 +msgid "Downmix to mono when encoding podcasts and audiobooks. Mono at lowbites sounds significantly better than stereo and cuts file sizes in half." +msgstr "编码播客和有声书时下混为单声道。低码率下单声道通常比立体声效果明显更好,并可将文件大小减半。" + +#: GUI/widgets/settingsPage.py:1775 +msgid "Enable separate quality settings for podcasts and audiobooks.Music tracks are unaffected." +msgstr "为播客和有声书启用独立质量设置。音乐曲目不受影响。" + +#: GUI/widgets/settingsPage.py:1795 +msgid "Enables a quality-enhancement post-processing pass in libfdk_aac. Improves output at the cost of slightly longer encode times. Only affects the libfdk_aac encoder." +msgstr "在 libfdk_aac 中启用质量增强后处理。会稍微增加编码时间,但可改善输出质量。仅影响 libfdk_aac 编码器。" + +#: GUI/widgets/settingsPage.py:1735 +msgid "Encode lossless sources (ALAC, FLAC, WAV, AIFF) as your selected lossy format instead of ALAC. Saves iPod storage at the cost of quality." +msgstr "将无损源文件(ALAC、FLAC、WAV、AIFF)编码为所选有损格式,而不是 ALAC。会节省 iPod 空间,但会牺牲音质。" + +#: GUI/widgets/settingsPage.py:1813 +msgid "Encodes stereo as Sum (Mid) and Difference (Side) rather than Left/Right. Concentrates bits where the signal is strongest, particularly on centred content. Affects the native aac encoder only." +msgstr "将立体声编码为和声道(Mid)与差声道(Side),而不是左/右声道。会把码率集中到信号最强的位置,尤其是居中的内容。仅影响原生 aac 编码器。" + +#: GUI/widgets/settingsPage.py:1647 +msgid "For portrait-heavy photos, rotate the device viewing caches clockwise when that uses more of the iPod's landscape photo screen. The original PC files are not modified." +msgstr "对于以竖图为主的照片,如果顺时针旋转设备查看缓存能更好利用 iPod 横向照片屏幕,就会进行旋转。电脑上的原始文件不会被修改。" + +#: GUI/widgets/settingsPage.py:1741 +msgid "Force existing MP3 and AAC files through the selected lossy encoder. Use this to make the synced library more uniform or smaller." +msgstr "强制将已有 MP3 和 AAC 文件重新经过所选有损编码器。可用于让同步后的资料库更统一或更小。" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Gold" +msgstr "金色" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Green" +msgstr "绿色" + +#: GUI/widgets/settingsPage.py:1702 +msgid "High Quality" +msgstr "高质量" + +#: GUI/widgets/settingsPage.py:1658 +msgid "Highest" +msgstr "最高" + +#: GUI/widgets/settingsPage.py:1658 +msgid "How to resolve rating conflicts when iPod and PC ratings differ. iPod/PC Wins uses that source (falling back to the other if zero). Highest/Lowest picks the max/min non-zero value. Average rounds to the nearest star." +msgstr "当 iPod 与电脑评分不一致时如何解决冲突。iPod/电脑优先会使用对应来源(若为 0 则回退到另一方)。最高/最低会选择非零最大/最小值。平均会四舍五入到最近的星级。" + +#: GUI/widgets/settingsPage.py:1572 +msgid "Ko-fi" +msgstr "Ko-fi" + +#: GUI/widgets/settingsPage.py:1495 +msgid "Light" +msgstr "浅色" + +#: GUI/widgets/settingsPage.py:1658 +msgid "Lowest" +msgstr "最低" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Match iPod" +msgstr "匹配 iPod" + +#: GUI/widgets/settingsPage.py:2008 +msgid "Max Backups" +msgstr "最大备份数" + +#: GUI/widgets/settingsPage.py:1787 +msgid "Maximum frequency the encoder will output. Lowering this (16–18 kHz) frees bits for the mid-range and can eliminate high-frequency squeaks at lower bitrates. Auto lets the encoder decide. Applies to all AAC encoders." +msgstr "编码器输出的最高频率。降低到 16-18 kHz 可为中频释放码率,并在低码率下减少高频尖锐噪声。自动会让编码器自行决定。适用于所有 AAC 编码器。" + +#: GUI/widgets/settingsPage.py:2008 +msgid "Maximum number of backup snapshots to keep per device. Oldest backups are automatically removed when the limit is exceeded." +msgstr "每台设备保留的备份快照最大数量。超过限制后会自动移除最旧的备份。" + +#: GUI/widgets/settingsPage.py:1819 +msgid "Merges high-frequency stereo bands into a single channel with direction metadata. Saves bits at low bitrates but can cause stereo image wobble. Affects the native aac encoder only." +msgstr "将高频立体声频带合并为带方向元数据的单声道。低码率下可节省码率,但可能造成声像晃动。仅影响原生 aac 编码器。" + +#: GUI/widgets/settingsPage.py:1507 GUI/widgets/settingsPage.py:1507 +msgid "Off" +msgstr "关闭" + +#: GUI/widgets/settingsPage.py:1507 +msgid "On" +msgstr "开启" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Orange" +msgstr "橙色" + +#: GUI/widgets/settingsPage.py:1619 +msgid "Overall concurrent sync work. This controls how many files can be prepared, fingerprinted, transcoded, or copied at once. Auto uses your CPU core count (capped at 8)." +msgstr "整体同步并发任务数。控制可同时准备、生成指纹、转码或复制的文件数量。自动会使用 CPU 核心数(最多 8)。" + +#: GUI/widgets/settingsPage.py:1658 +msgid "PC Wins" +msgstr "电脑优先" + +#: GUI/widgets/settingsPage.py:1893 +msgid "Path Overrides" +msgstr "路径覆盖" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Pink" +msgstr "粉色" + +#: GUI/widgets/settingsPage.py:1882 +msgid "Point to a custom ffmpeg binary. Leave empty to auto-detect." +msgstr "指向自定义 ffmpeg 可执行文件。留空则自动检测。" + +#: GUI/widgets/settingsPage.py:1887 +msgid "Point to a custom fpcalc binary. Leave empty to auto-detect." +msgstr "指向自定义 fpcalc 可执行文件。留空则自动检测。" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Purple" +msgstr "紫色" + +#: GUI/widgets/settingsPage.py:1751 +msgid "Quality level for H.264 video transcodes. Lower CRF = better quality but larger files. Resolution and codec are always forced to iPod-compatible values." +msgstr "H.264 视频转码质量等级。CRF 越低质量越好,但文件越大。分辨率和编码格式始终会强制为 iPod 兼容值。" + +#: GUI/widgets/settingsPage.py:1722 +msgid "Quality level for VBR encoding. Higher = better quality, larger files." +msgstr "VBR 编码质量等级。越高质量越好,文件也越大。" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Red" +msgstr "红色" + +#: GUI/widgets/settingsPage.py:1807 +msgid "Replaces noise-like frequency bands with synthetic noise, saving bits. Can cause sandpaper or hissing artifacts if the encoder mistakes tonal content for noise. Off by default. Affects the native aac encoder only." +msgstr "用合成噪声替代类似噪声的频段以节省码率。如果编码器把有音调的内容误判为噪声,可能产生砂纸感或嘶声伪影。默认关闭。仅影响原生 aac 编码器。" + +#: GUI/widgets/settingsPage.py:1875 +msgid "Required for acoustic fingerprinting, which identifies tracks even after re-encoding." +msgstr "声学指纹所必需,用于在重新编码后仍能识别曲目。" + +#: GUI/widgets/settingsPage.py:1869 +msgid "Required for transcoding and media probing. Includes ffmpeg and ffprobe." +msgstr "转码和媒体探测所必需。包含 ffmpeg 和 ffprobe。" + +#: GUI/widgets/settingsPage.py:1902 +msgid "Scrobble on Sync" +msgstr "同步时记录播放" + +#: GUI/widgets/settingsPage.py:1928 +msgid "Services" +msgstr "服务" + +#: GUI/widgets/settingsPage.py:1801 +msgid "Shapes quantization noise around transients to reduce pre-echo (smearing before drum hits). Disabling saves a few bits per block. Affects the native aac encoder only." +msgstr "围绕瞬态调整量化噪声,以减少预回声(鼓点前的拖影)。禁用后每个块可节省少量码率。仅影响原生 aac 编码器。" + +#: GUI/widgets/settingsPage.py:1627 +msgid "Simultaneous writes to the iPod filesystem. Set to 1 for HDD-based iPods to reduce fragmentation risk. Auto uses an HDD-safe default when the device looks like a hard-drive iPod." +msgstr "同时写入 iPod 文件系统的任务数。对于硬盘版 iPod,设为 1 可降低碎片风险。自动会在设备看起来像硬盘版 iPod 时使用硬盘安全默认值。" + +#: GUI/widgets/settingsPage.py:1762 +msgid "Slower presets produce slightly better quality at the same CRF, but take much longer." +msgstr "较慢的预设在相同 CRF 下质量略好,但耗时会长得多。" + +#: GUI/widgets/podcastBrowser.py:1540 +msgid "Subscriptions" +msgstr "订阅" + +#: GUI/widgets/settingsPage.py:1495 GUI/widgets/settingsPage.py:1507 +msgid "System" +msgstr "系统" + +#: GUI/widgets/settingsPage.py:1716 +msgid "Target CBR bitrate for music tracks." +msgstr "音乐曲目的目标 CBR 码率。" + +#: GUI/widgets/settingsPage.py:1728 +msgid "Target bitrate for podcasts and audiobooks (Always uses CBR despite set Bitrate Mode). Pair with 'Mono for Spoken Word' for best results." +msgstr "播客和有声书的目标码率(无论码率模式如何,始终使用 CBR)。配合“语音内容转单声道”效果最佳。" + +#: GUI/widgets/settingsPage.py:1522 +msgid "Teal" +msgstr "青色" + +#: GUI/widgets/settingsPage.py:1950 GUI/widgets/settingsPage.py:2008 +msgid "Unlimited" +msgstr "无限制" + +#: GUI/widgets/settingsPage.py:1653 +msgid "Use aspect-fit for device photo thumbnail formats. When off (default), thumbnails use iTunes-style crop-to-fill." +msgstr "设备照片缩略图使用等比适配。关闭时(默认),缩略图会使用 iTunes 风格的裁剪填充。" + +#: GUI/widgets/settingsPage.py:1709 +msgid "VBR" +msgstr "VBR" + +#: GUI/widgets/settingsPage.py:1746 +msgid "When enabled, WAV files are converted to ALAC instead of copied. Prefer Lossy Encoding overrides this and converts WAV to the selected lossy format." +msgstr "启用后,WAV 文件会转换为 ALAC,而不是直接复制。“优先有损编码”会覆盖此设置,并将 WAV 转换为所选有损格式。" + +#: GUI/widgets/settingsPage.py:1995 +msgid "Where full device backups are stored on your PC. Leave empty for the platform default." +msgstr "完整设备备份在电脑上的存放位置。留空则使用平台默认位置。" + +#: GUI/widgets/settingsPage.py:1635 +msgid "While syncing, write ratings and sound check values into your PC music files. When off, no changes are made to your PC files." +msgstr "同步时,将评分和音量均衡值写入电脑音乐文件。关闭时,不会修改电脑文件。" + +#: GUI/widgets/settingsPage.py:1688 +msgid "aac" +msgstr "aac" + +#: GUI/widgets/settingsPage.py:1688 +msgid "aac_at" +msgstr "aac_at" + +#: GUI/widgets/settingsPage.py:1488 +msgid "en" +msgstr "en" + +#: GUI/widgets/settingsPage.py:1762 GUI/widgets/settingsPage.py:1762 +msgid "fast" +msgstr "fast" + +#: GUI/widgets/settingsPage.py:1658 GUI/widgets/settingsPage.py:1658 +msgid "iPod Wins" +msgstr "iPod 优先" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libfdk_aac" +msgstr "libfdk_aac" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libmp3lame" +msgstr "libmp3lame" + +#: GUI/widgets/settingsPage.py:1688 +msgid "libshine" +msgstr "libshine" + +#: GUI/widgets/settingsPage.py:1762 +msgid "medium" +msgstr "medium" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q0 (Best)" +msgstr "q0(最佳)" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q1" +msgstr "q1" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q2" +msgstr "q2" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q3" +msgstr "q3" + +#: GUI/widgets/settingsPage.py:1722 GUI/widgets/settingsPage.py:1722 +msgid "q4" +msgstr "q4" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q5" +msgstr "q5" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q6" +msgstr "q6" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q7" +msgstr "q7" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q8" +msgstr "q8" + +#: GUI/widgets/settingsPage.py:1722 +msgid "q9 (Smallest)" +msgstr "q9(最小)" + +#: GUI/widgets/settingsPage.py:1762 +msgid "slow" +msgstr "slow" + +#: GUI/widgets/settingsPage.py:1762 +msgid "ultrafast" +msgstr "ultrafast" + +#: GUI/widgets/settingsPage.py:1762 +msgid "veryfast" +msgstr "veryfast" + + +#: GUI/widgets/settingsPage.py:1780 +msgid "Always output audio at 44.1 kHz (CD rate). Recommended for early iPods (1G-4G) that can have trouble with 48 kHz ALAC. When off, sample rate is reduced to 48 kHz as iPods can only decode 48 kHz or lower" +msgstr "始终以 44.1 kHz(CD 采样率)输出音频。建议用于早期 iPod(1G-4G),它们可能无法很好处理 48 kHz ALAC。关闭时,采样率会降到 48 kHz,因为 iPod 只能解码 48 kHz 或更低。" + +#: GUI/widgets/settingsPage.py:1688 +msgid "Choose which lossy encoder to use. Auto chooses the best available." +msgstr "选择要使用的有损编码器。自动会选择可用的最佳编码器。" + +#: GUI/widgets/settingsPage.py:1775 +msgid "Enable separate quality settings for podcasts and audiobooks. Music tracks are unaffected." +msgstr "为播客和有声书启用独立质量设置。音乐曲目不受影响。" + + +#: GUI/widgets/playlistBrowser.py:1667 +msgid "New Smart Playlist" +msgstr "新建智能播放列表" + + +#: GUI/widgets/podcastBrowser.py:1441 +msgid "Add Podcast" +msgstr "添加播客" + +#: GUI/widgets/podcastBrowser.py:1531 +msgid "Add Your First Podcast" +msgstr "添加第一个播客" + +#: GUI/widgets/podcastBrowser.py:451 +msgid "Add this episode to iPod" +msgstr "将这一集添加到 iPod" + +#: GUI/widgets/podcastBrowser.py:469 GUI/widgets/podcastBrowser.py:449 GUI/widgets/podcastBrowser.py:2102 +msgid "Add to iPod" +msgstr "添加到 iPod" + +#: GUI/widgets/podcastBrowser.py:1469 +msgid "Apply per-feed settings: remove listened/old episodes, fill empty slots with new episodes" +msgstr "应用每个 feed 的设置:移除已听/过旧剧集,并用新剧集填补空位" + +#: GUI/widgets/podcastBrowser.py:235 +msgid "Downloaded" +msgstr "已下载" + +#: GUI/widgets/podcastBrowser.py:2247 +msgid "Downloaded: {count}" +msgstr "已下载:{count}" + +#: GUI/widgets/podcastBrowser.py:260 +msgid "Episode" +msgstr "剧集" + +#: GUI/widgets/podcastBrowser.py:2244 +msgid "Episodes: {count}" +msgstr "剧集:{count}" + +#: GUI/widgets/podcastBrowser.py:516 GUI/widgets/podcastBrowser.py:596 GUI/widgets/podcastBrowser.py:525 +msgid "More" +msgstr "更多" + +#: GUI/widgets/podcastBrowser.py:1508 +msgid "No Podcast Subscriptions" +msgstr "没有播客订阅" + +#: GUI/widgets/podcastBrowser.py:586 +msgid "No description available." +msgstr "暂无描述。" + +#: GUI/widgets/podcastBrowser.py:233 +msgid "On iPod" +msgstr "已在 iPod 上" + +#: GUI/widgets/podcastBrowser.py:2250 +msgid "On iPod: {count}" +msgstr "iPod 上:{count}" + +#: GUI/widgets/podcastBrowser.py:2240 +msgid "RSS feed linked" +msgstr "已关联 RSS feed" + +#: GUI/widgets/podcastBrowser.py:1451 +msgid "Refresh All" +msgstr "全部刷新" + +#: GUI/widgets/podcastBrowser.py:2024 +msgid "Refresh Feed" +msgstr "刷新 Feed" + +#: GUI/widgets/podcastBrowser.py:2109 +msgid "Remove Download" +msgstr "移除下载" + +#: GUI/widgets/podcastBrowser.py:504 GUI/widgets/podcastBrowser.py:480 GUI/widgets/podcastBrowser.py:2116 +msgid "Remove from iPod" +msgstr "从 iPod 移除" + +#: GUI/widgets/podcastBrowser.py:484 +msgid "Remove this episode from iPod" +msgstr "从 iPod 移除这一集" + +#: GUI/widgets/podcastBrowser.py:1518 +msgid "" +"Search for podcasts or add an RSS feed to get started.\n" +"Episodes can be downloaded and synced to your iPod." +msgstr "" +"搜索播客或添加 RSS feed 即可开始。\n" +"剧集可以下载并同步到你的 iPod。" + +#: GUI/widgets/podcastBrowser.py:1648 GUI/widgets/podcastBrowser.py:2209 +msgid "Select a podcast" +msgstr "选择播客" + +#: GUI/widgets/podcastBrowser.py:596 GUI/widgets/podcastBrowser.py:526 +msgid "Show less" +msgstr "收起" + +#: GUI/widgets/podcastBrowser.py:1461 +msgid "Sync Podcasts" +msgstr "同步播客" + +#: GUI/widgets/podcastBrowser.py:2226 +msgid "Unknown Author" +msgstr "未知作者" + +#: GUI/widgets/podcastBrowser.py:2026 +msgid "Unsubscribe" +msgstr "取消订阅" + +#: GUI/widgets/podcastBrowser.py:637 GUI/widgets/podcastBrowser.py:584 +msgid "Untitled Episode" +msgstr "未命名剧集" + +#: GUI/widgets/podcastBrowser.py:2225 +msgid "Untitled Podcast" +msgstr "未命名播客" + +#: GUI/widgets/podcastBrowser.py:2238 +msgid "Updated {date}" +msgstr "更新于 {date}" + + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Day" +msgstr "1 天" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Month" +msgstr "1 个月" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "1 Week" +msgstr "1 周" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "2 Months" +msgstr "2 个月" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "2 Weeks" +msgstr "2 周" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "3 Days" +msgstr "3 天" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "3 Months" +msgstr "3 个月" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "Adding podcast…" +msgstr "正在添加播客…" + +#: GUI/widgets/podcastBrowser.py:2547 +msgid "All podcasts are up to date" +msgstr "所有播客均为最新" + +#: GUI/widgets/podcastBrowser.py:2825 +msgid "All selected episodes are already on iPod" +msgstr "所选剧集已在 iPod 上" + +#: GUI/widgets/podcastBrowser.py:2581 +msgid "Already subscribed" +msgstr "已订阅" + +#: GUI/widgets/podcastSearchDialog.py:192 +msgid "Checking podcast results now." +msgstr "正在检查播客结果。" + +#: GUI/widgets/podcastBrowser.py:2371 +msgid "Checking subscribed feeds for new episodes." +msgstr "正在检查已订阅 feed 的新剧集。" + +#: GUI/widgets/podcastBrowser.py:2671 +msgid "Checking the feed for the latest episodes." +msgstr "正在检查 feed 中的最新剧集。" + +#: GUI/widgets/podcastBrowser.py:1775 +msgid "Clear method:" +msgstr "清理方式:" + +#: GUI/widgets/podcastBrowser.py:1777 +msgid "Clear older than:" +msgstr "清理早于:" + +#: GUI/widgets/podcastBrowser.py:1776 +msgid "Clear when listened:" +msgstr "听完后清理:" + +#: GUI/widgets/podcastBrowser.py:2657 +msgid "Could not add podcast" +msgstr "无法添加播客" + +#: GUI/widgets/podcastBrowser.py:2931 +msgid "Episodes not found on iPod" +msgstr "未在 iPod 上找到剧集" + +#: GUI/widgets/podcastBrowser.py:1773 +msgid "Episodes:" +msgstr "剧集:" + +#: GUI/widgets/podcastBrowser.py:2584 +msgid "Fetching feed…" +msgstr "正在获取 feed…" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "Fetching the feed and latest episodes." +msgstr "正在获取 feed 和最新剧集。" + +#: GUI/widgets/podcastBrowser.py:1774 +msgid "Fill with:" +msgstr "填充方式:" + +#: GUI/widgets/podcastSearchDialog.py:118 +msgid "Find podcasts" +msgstr "查找播客" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "Immediately" +msgstr "立即" + +#: GUI/widgets/podcastBrowser.py:2330 +msgid "Loading episodes…" +msgstr "正在加载剧集…" + +#: GUI/widgets/podcastBrowser.py:1765 +msgid "Mark for Replacement" +msgstr "标记为替换" + +#: GUI/widgets/podcastBrowser.py:1768 +msgid "Never" +msgstr "从不" + +#: GUI/widgets/podcastBrowser.py:1764 +msgid "Newest Episode" +msgstr "最新剧集" + +#: GUI/widgets/podcastBrowser.py:1764 +msgid "Next Episode" +msgstr "下一集" + +#: GUI/widgets/podcastBrowser.py:2281 +msgid "No episodes found" +msgstr "未找到剧集" + +#: GUI/widgets/podcastBrowser.py:2815 +msgid "No episodes to sync" +msgstr "没有可同步的剧集" + +#: GUI/widgets/podcastBrowser.py:2772 GUI/widgets/podcastBrowser.py:2899 +msgid "No iPod connected" +msgstr "未连接 iPod" + +#: GUI/widgets/podcastSearchDialog.py:213 +msgid "No podcasts found" +msgstr "未找到播客" + +#: GUI/widgets/podcastBrowser.py:2366 +msgid "No subscriptions to refresh" +msgstr "没有可刷新的订阅" + +#: GUI/widgets/podcastBrowser.py:2476 +msgid "No subscriptions to sync" +msgstr "没有可同步的订阅" + +#: GUI/widgets/podcastBrowser.py:2446 +msgid "Podcasts could not refresh" +msgstr "播客刷新失败" + +#: GUI/widgets/podcastBrowser.py:2481 +msgid "Preparing podcast sync…" +msgstr "正在准备播客同步…" + +#: GUI/widgets/podcastBrowser.py:2457 +msgid "Refresh failed" +msgstr "刷新失败" + +#: GUI/widgets/podcastBrowser.py:2481 +msgid "Refreshing feeds before building the sync plan." +msgstr "正在构建同步计划前刷新 feed。" + +#: GUI/widgets/podcastBrowser.py:2480 +msgid "Refreshing feeds for sync…" +msgstr "正在为同步刷新 feed…" + +#: GUI/widgets/podcastBrowser.py:2371 +msgid "Refreshing podcasts…" +msgstr "正在刷新播客…" + +#: GUI/widgets/podcastBrowser.py:2671 +msgid "Refreshing this podcast…" +msgstr "正在刷新此播客…" + +#: GUI/widgets/podcastBrowser.py:1765 +msgid "Remove Immediately" +msgstr "立即移除" + +#: GUI/widgets/podcastSearchDialog.py:118 +msgid "Search by show name, or paste an RSS feed below." +msgstr "按节目名称搜索,或在下方粘贴 RSS feed。" + +#: GUI/widgets/podcastSearchDialog.py:192 +msgid "Searching for podcasts…" +msgstr "正在搜索播客…" + +#: GUI/widgets/podcastBrowser.py:2756 +msgid "Select episodes first" +msgstr "请先选择剧集" + +#: GUI/widgets/podcastBrowser.py:2784 +msgid "Selected episodes are already on iPod" +msgstr "所选剧集已在 iPod 上" + +#: GUI/widgets/podcastBrowser.py:2786 +msgid "Selected episodes cannot be added yet" +msgstr "所选剧集暂时无法添加" + +#: GUI/widgets/podcastBrowser.py:2448 +msgid "Some podcasts could not refresh" +msgstr "部分播客刷新失败" + +#: GUI/widgets/podcastBrowser.py:2196 +msgid "Subscribed shows will appear here after their feeds refresh." +msgstr "订阅节目会在 feed 刷新后显示在这里。" + +#: GUI/widgets/podcastBrowser.py:2569 +msgid "Sync failed" +msgstr "同步失败" + +#: GUI/widgets/podcastBrowser.py:2471 GUI/widgets/podcastBrowser.py:2769 +msgid "This iPod does not support podcasts" +msgstr "此 iPod 不支持播客" + +#: GUI/widgets/podcastBrowser.py:2281 +msgid "This podcast loaded, but its feed did not list any episodes." +msgstr "此播客已加载,但它的 feed 未列出任何剧集。" + +#: GUI/widgets/podcastSearchDialog.py:213 +msgid "Try a different show name, or paste the RSS feed directly." +msgstr "请尝试其他节目名称,或直接粘贴 RSS feed。" + +#: GUI/widgets/podcastBrowser.py:2196 +msgid "Waiting for episodes" +msgstr "等待剧集" + + +#: GUI/widgets/podcastBrowser.py:2895 GUI/widgets/podcastBrowser.py:2896 +msgid "Failed: {error}" +msgstr "失败:{error}" + +#: GUI/widgets/podcastBrowser.py:2593 GUI/widgets/podcastBrowser.py:2594 +msgid "Podcast sync: {summary}" +msgstr "播客同步:{summary}" + +#: GUI/widgets/podcastBrowser.py:2425 +msgid "Refreshed {count} feed" +msgstr "已刷新 {count} 个 feed" + +#: GUI/widgets/podcastBrowser.py:2427 +msgid "Refreshed {count} feeds" +msgstr "已刷新 {count} 个 feed" + +#: GUI/widgets/podcastBrowser.py:2454 +msgid "Refreshed {count}; {failures} feed could not update" +msgstr "已刷新 {count} 个;{failures} 个 feed 更新失败" + +#: GUI/widgets/podcastBrowser.py:2459 +msgid "Refreshed {count}; {failures} feeds could not update" +msgstr "已刷新 {count} 个;{failures} 个 feed 更新失败" + +#: GUI/widgets/podcastBrowser.py:2745 GUI/widgets/podcastBrowser.py:2746 +msgid "Refreshed {title}" +msgstr "已刷新 {title}" + +#: GUI/widgets/podcastBrowser.py:2374 +msgid "Refreshing {count} feeds…" +msgstr "正在刷新 {count} 个 feed…" + +#: GUI/widgets/podcastBrowser.py:2372 +msgid "Refreshing {count} feed…" +msgstr "正在刷新 {count} 个 feed…" + +#: GUI/widgets/podcastBrowser.py:2715 GUI/widgets/podcastBrowser.py:2716 +msgid "Refreshing {title}…" +msgstr "正在刷新 {title}…" + +#: GUI/widgets/podcastBrowser.py:2942 +msgid "Removed {count} download" +msgstr "已移除 {count} 个下载" + +#: GUI/widgets/podcastBrowser.py:2944 +msgid "Removed {count} downloads" +msgstr "已移除 {count} 个下载" + +#: GUI/widgets/podcastBrowser.py:2881 +msgid "Sending {count} episode to sync…" +msgstr "正在发送 {count} 个剧集进行同步…" + +#: GUI/widgets/podcastBrowser.py:2883 +msgid "Sending {count} episodes to sync…" +msgstr "正在发送 {count} 个剧集进行同步…" + +#: GUI/widgets/podcastBrowser.py:3005 +msgid "Sending {count} removal to sync…" +msgstr "正在发送 {count} 个移除项进行同步…" + +#: GUI/widgets/podcastBrowser.py:3007 +msgid "Sending {count} removals to sync…" +msgstr "正在发送 {count} 个移除项进行同步…" + +#: GUI/widgets/podcastBrowser.py:2675 GUI/widgets/podcastBrowser.py:2676 +msgid "Subscribed to {title}" +msgstr "已订阅 {title}" + +#: GUI/widgets/podcastBrowser.py:2705 GUI/widgets/podcastBrowser.py:2706 +msgid "Unsubscribed from {title}" +msgstr "已取消订阅 {title}" + +#: GUI/widgets/podcastBrowser.py:2590 +msgid "{count} to add" +msgstr "要添加 {count} 个" + +#: GUI/widgets/podcastBrowser.py:2586 +msgid "{count} to remove" +msgstr "要移除 {count} 个" + +#: GUI/widgets/playlistBrowser.py +msgid "Music" +msgstr "音乐" + +#: GUI/widgets/playlistBrowser.py +msgid "Ringtones" +msgstr "铃声" + +#: GUI/widgets/playlistBrowser.py +msgid "Rentals" +msgstr "租借影片" diff --git a/pyproject.toml b/pyproject.toml index 523c12e..59068f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ only-include = [ "assets/glyphs", "assets/icons", "assets/ipod_images", + "locale", ] [tool.flake8] diff --git a/scripts/compile_translations.py b/scripts/compile_translations.py new file mode 100644 index 0000000..36db4ce --- /dev/null +++ b/scripts/compile_translations.py @@ -0,0 +1,121 @@ +"""Compile gettext .po files to .mo files. + +The project only needs a small subset of gettext catalog features for the GUI +strings: singular ``msgid`` / ``msgstr`` entries with standard Python escaping. +Keeping this script local avoids requiring GNU gettext on contributor machines. +""" + +from __future__ import annotations + +import ast +import struct +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +LOCALE_ROOT = PROJECT_ROOT / "locale" +DOMAIN = "iopenpod" + + +def _unquote_po_string(line: str) -> str: + return ast.literal_eval(line.strip()) + + +def _read_po(path: Path) -> dict[str, str]: + messages: dict[str, str] = {} + msgid: str | None = None + msgstr: str | None = None + active: str | None = None + + def flush() -> None: + nonlocal msgid, msgstr, active + if msgid is not None and msgstr is not None: + messages[msgid] = msgstr + msgid = None + msgstr = None + active = None + + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line: + flush() + continue + if line.startswith("#"): + continue + if line.startswith("msgid "): + if msgid is not None and msgstr is not None: + flush() + msgid = _unquote_po_string(line[6:]) + msgstr = None + active = "msgid" + continue + if line.startswith("msgstr "): + msgstr = _unquote_po_string(line[7:]) + active = "msgstr" + continue + if line.startswith('"') and active == "msgid" and msgid is not None: + msgid += _unquote_po_string(line) + continue + if line.startswith('"') and active == "msgstr" and msgstr is not None: + msgstr += _unquote_po_string(line) + continue + + flush() + return messages + + +def _write_mo(messages: dict[str, str], path: Path) -> None: + keys = sorted(messages) + ids = b"" + strs = b"" + offsets: list[tuple[int, int, int, int]] = [] + + for key in keys: + msgid = key.encode("utf-8") + msgstr = messages[key].encode("utf-8") + offsets.append((len(msgid), len(ids), len(msgstr), len(strs))) + ids += msgid + b"\0" + strs += msgstr + b"\0" + + count = len(keys) + keystart = 7 * 4 + valuestart = keystart + count * 8 + ids_offset = valuestart + count * 8 + strs_offset = ids_offset + len(ids) + + output = [ + struct.pack( + "Iiiiiii", + 0x950412DE, + 0, + count, + keystart, + valuestart, + 0, + 0, + ) + ] + + for msgid_len, msgid_offset, _msgstr_len, _msgstr_offset in offsets: + output.append(struct.pack("ii", msgid_len, ids_offset + msgid_offset)) + for _msgid_len, _msgid_offset, msgstr_len, msgstr_offset in offsets: + output.append(struct.pack("ii", msgstr_len, strs_offset + msgstr_offset)) + output.append(ids) + output.append(strs) + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"".join(output)) + + +def compile_catalogs() -> list[Path]: + outputs: list[Path] = [] + for po_path in sorted(LOCALE_ROOT.glob(f"*/LC_MESSAGES/{DOMAIN}.po")): + messages = _read_po(po_path) + mo_path = po_path.with_suffix(".mo") + _write_mo(messages, mo_path) + outputs.append(mo_path) + return outputs + + +if __name__ == "__main__": + for output_path in compile_catalogs(): + print(output_path.relative_to(PROJECT_ROOT)) diff --git a/scripts/update_translations.py b/scripts/update_translations.py new file mode 100644 index 0000000..1db3905 --- /dev/null +++ b/scripts/update_translations.py @@ -0,0 +1,221 @@ +"""Extract gettext msgids from source and update locale catalogs. + +The app uses English source strings wrapped in ``tr(...)`` or the conventional +``_(...)`` alias. This script keeps ``locale/*/LC_MESSAGES/iopenpod.po`` files +in sync without requiring GNU gettext or Babel on contributor machines. +""" + +from __future__ import annotations + +import argparse +import ast +import json +import sys +from collections import defaultdict +from pathlib import Path + +from scripts.compile_translations import DOMAIN, LOCALE_ROOT, PROJECT_ROOT, _read_po + +SOURCE_ROOTS = ( + "GUI", + "app_core", + "infrastructure", + "main.py", + "sync_progress_stages.py", +) +TRANSLATION_FUNCTIONS = {"_", "tr"} +I18N_LITERAL_ARG_CALLS = { + "ActionRow": {0, 1}, + "ComboRow": {0, 1}, + "FileRow": {0, 1}, + "FolderRow": {0, 1}, + "ResettableFolderRow": {0, 1}, + "SettingRow": {0, 1}, + "SpinRow": {0, 1}, + "ToggleRow": {0, 1}, + "ToolRow": {0, 1}, + "_LastFmAuthRow": {0, 1}, + "_TokenRow": {0, 1}, + "BrowserPane": {0}, + "setTitle": {0}, + "_make_pair": {0}, + "_make_setting_combo": {0}, + "_make_setting_label": {0}, + "_set_action_status": {0}, + "_set_status": {0}, + "_show_episode_empty": {0, 1}, + "_show_episode_loading": {0, 1}, + "show_empty": {0, 1}, + "show_error": {0, 1}, + "show_loading": {0, 1}, +} +I18N_LITERAL_KEYWORDS = { + "button_text", + "current", + "default_label", + "options", +} +I18N_ALL_LITERAL_ARG_CALLS = {"_make_page"} + + +def _source_files() -> list[Path]: + files: list[Path] = [] + for root_name in SOURCE_ROOTS: + root = PROJECT_ROOT / root_name + if root.is_file(): + files.append(root) + elif root.is_dir(): + files.extend(sorted(root.rglob("*.py"))) + return sorted(files) + + +def _call_name(node: ast.AST) -> str | None: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + return node.attr + return None + + +def _literal_strings(node: ast.AST) -> list[str]: + """Return literal string constants nested under *node*.""" + + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return [node.value] + if isinstance(node, ast.JoinedStr): + return [] + values: list[str] = [] + for child in ast.iter_child_nodes(node): + values.extend(_literal_strings(child)) + return values + + +def _add_message( + messages: dict[str, list[str]], + msgid: str, + rel_path: Path, + lineno: int, +) -> None: + messages[msgid].append(f"{rel_path}:{lineno}") + + +def extract_messages(paths: list[Path] | None = None) -> dict[str, list[str]]: + """Return msgid -> source locations for literal translation calls.""" + + messages: dict[str, list[str]] = defaultdict(list) + for path in paths or _source_files(): + try: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + except SyntaxError as exc: + raise SystemExit(f"Could not parse {path}: {exc}") from exc + + try: + rel_path = path.relative_to(PROJECT_ROOT) + except ValueError: + rel_path = path + for node in ast.walk(tree): + if not isinstance(node, ast.Call) or not node.args: + continue + call_name = _call_name(node.func) + if call_name in TRANSLATION_FUNCTIONS: + first_arg = node.args[0] + if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str): + _add_message(messages, first_arg.value, rel_path, node.lineno) + continue + + literal_arg_indexes = I18N_LITERAL_ARG_CALLS.get(call_name or "") + if literal_arg_indexes is not None: + for index in literal_arg_indexes: + if index < len(node.args): + for msgid in _literal_strings(node.args[index]): + _add_message(messages, msgid, rel_path, node.lineno) + elif call_name in I18N_ALL_LITERAL_ARG_CALLS: + for arg in node.args: + for msgid in _literal_strings(arg): + _add_message(messages, msgid, rel_path, node.lineno) + + if call_name in I18N_LITERAL_ARG_CALLS: + for keyword in node.keywords: + if keyword.arg in I18N_LITERAL_KEYWORDS: + for msgid in _literal_strings(keyword.value): + _add_message(messages, msgid, rel_path, node.lineno) + + return dict(sorted(messages.items(), key=lambda item: item[0])) + + +def _po_literal(text: str) -> str: + return json.dumps(text, ensure_ascii=False) + + +def _po_field(keyword: str, text: str) -> list[str]: + if "\n" not in text: + return [f"{keyword} {_po_literal(text)}"] + lines = [f"{keyword} \"\""] + lines.extend(_po_literal(part) for part in text.splitlines(keepends=True)) + return lines + + +def _format_entry(msgid: str, locations: list[str]) -> str: + refs = " ".join(locations[:8]) + lines = [f"#: {refs}"] if refs else [] + if len(locations) > 8: + lines.append(f"#. Additional references: {len(locations) - 8}") + lines.extend(_po_field("msgid", msgid)) + lines.append('msgstr ""') + return "\n".join(lines) + + +def update_catalog(po_path: Path, messages: dict[str, list[str]], *, check: bool) -> list[str]: + """Append missing msgids to a catalog, or return them in check mode.""" + + existing = _read_po(po_path) if po_path.exists() else {} + missing = [msgid for msgid in messages if msgid not in existing] + if check or not missing: + return missing + + original = po_path.read_text(encoding="utf-8") if po_path.exists() else "" + separator = "\n\n" if original and not original.endswith("\n\n") else "" + entries = "\n\n".join(_format_entry(msgid, messages[msgid]) for msgid in missing) + po_path.parent.mkdir(parents=True, exist_ok=True) + po_path.write_text(f"{original}{separator}{entries}\n", encoding="utf-8") + return missing + + +def _catalog_paths() -> list[Path]: + return sorted(LOCALE_ROOT.glob(f"*/LC_MESSAGES/{DOMAIN}.po")) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="Fail if any catalog is missing extracted msgids.", + ) + args = parser.parse_args(argv) + + messages = extract_messages() + catalog_paths = _catalog_paths() + if not catalog_paths: + print("No locale catalogs found.", file=sys.stderr) + return 1 + + failed = False + for po_path in catalog_paths: + missing = update_catalog(po_path, messages, check=args.check) + rel_path = po_path.relative_to(PROJECT_ROOT) + if missing: + print(f"{rel_path}: {len(missing)} missing msgids") + for msgid in missing[:20]: + print(f" - {msgid!r}") + if len(missing) > 20: + print(f" ... {len(missing) - 20} more") + failed = failed or args.check + else: + print(f"{rel_path}: up to date") + + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_i18n.py b/tests/test_i18n.py new file mode 100644 index 0000000..88697f5 --- /dev/null +++ b/tests/test_i18n.py @@ -0,0 +1,135 @@ +from PyQt6.QtWidgets import QSplitter + +from GUI.widgets.trackListTitleBar import TrackListTitleBar +from infrastructure.i18n import ( + LANGUAGE_DE, + LANGUAGE_EN, + LANGUAGE_ES, + LANGUAGE_FR, + LANGUAGE_ZH, + LOCALE_DIR, + language_from_display_name, + language_options, + normalize_language, + set_language, + tr, +) + + +def test_language_normalization_accepts_aliases() -> None: + assert normalize_language("zh_CN") == LANGUAGE_ZH + assert normalize_language("zh-Hans") == LANGUAGE_ZH + assert normalize_language("en_US") == LANGUAGE_EN + assert normalize_language("de_DE") == LANGUAGE_DE + assert normalize_language("Deutsch") == LANGUAGE_DE + assert normalize_language("fr_FR") == LANGUAGE_FR + assert normalize_language("Français") == LANGUAGE_FR + assert normalize_language("es_ES") == LANGUAGE_ES + assert normalize_language("Español") == LANGUAGE_ES + assert normalize_language("unsupported") == LANGUAGE_EN + + +def test_translation_uses_active_language_and_falls_back_to_source() -> None: + try: + set_language("zh") + assert tr("Settings") == "设置" + assert tr("Cache Location") == "缓存位置" + assert tr("Select an Album") == "选择专辑" + assert tr("All Tracks") == "全部曲目" + assert tr("Artist") == "艺人" + assert tr("Album") == "专辑" + assert tr("Music") == "音乐" + assert tr("Ringtones") == "铃声" + assert tr("Rentals") == "租借影片" + assert tr("Name") == "名称" + assert tr("Date Added") == "添加日期" + assert tr("Scrobble on Sync") == "同步时记录播放" + assert tr("Auto") == "自动" + assert tr("Balanced") == "均衡" + assert tr("iPod Wins") == "iPod 优先" + assert tr("Add Podcast") == "添加播客" + assert tr("Refresh All") == "全部刷新" + assert tr("Sync Podcasts") == "同步播客" + assert tr("No Podcast Subscriptions") == "没有播客订阅" + assert tr("Add Your First Podcast") == "添加第一个播客" + assert tr("{count:,} songs").format(count=2) == "2 首歌曲" + assert tr("Untranslated source") == "Untranslated source" + + set_language("en") + assert tr("Settings") == "Settings" + finally: + set_language("en") + + +def test_language_selector_display_names_map_to_codes() -> None: + assert language_options() == ["English", "中文", "Deutsch", "Français", "Español"] + assert language_from_display_name("English") == LANGUAGE_EN + assert language_from_display_name("中文") == LANGUAGE_ZH + assert language_from_display_name("Deutsch") == LANGUAGE_DE + assert language_from_display_name("Français") == LANGUAGE_FR + assert language_from_display_name("Español") == LANGUAGE_ES + + +def test_chinese_gettext_catalog_is_compiled() -> None: + assert (LOCALE_DIR / "zh_CN" / "LC_MESSAGES" / "iopenpod.mo").is_file() + + +def test_additional_gettext_catalogs_are_compiled() -> None: + assert (LOCALE_DIR / "de" / "LC_MESSAGES" / "iopenpod.mo").is_file() + assert (LOCALE_DIR / "fr" / "LC_MESSAGES" / "iopenpod.mo").is_file() + assert (LOCALE_DIR / "es" / "LC_MESSAGES" / "iopenpod.mo").is_file() + + +def test_additional_language_catalogs_translate_core_strings() -> None: + try: + set_language("de") + assert tr("Settings") == "Einstellungen" + assert tr("Add Podcast") == "Podcast hinzufügen" + assert tr("No Podcast Subscriptions") == "Keine Podcast-Abonnements" + + set_language("fr") + assert tr("Settings") == "Paramètres" + assert tr("Add Podcast") == "Ajouter un podcast" + assert tr("No Podcast Subscriptions") == "Aucun abonnement aux podcasts" + + set_language("es") + assert tr("Settings") == "Configuración" + assert tr("Add Podcast") == "Añadir podcast" + assert tr("No Podcast Subscriptions") == "No hay suscripciones a podcasts" + finally: + set_language("en") + + +def test_track_title_bar_leaves_dynamic_titles_untranslated(qtbot) -> None: + try: + set_language("zh") + splitter = QSplitter() + qtbot.addWidget(splitter) + title_bar = TrackListTitleBar(splitter) + qtbot.addWidget(title_bar) + + title_bar.setTitle("Settings") + assert title_bar.title.text() == "Settings" + + title_bar.setTitle("Settings", translate=True) + assert title_bar.title.text() == "设置" + finally: + set_language("en") + + +def test_track_list_formatters_use_active_language() -> None: + from GUI.widgets.MBListView import ( + format_bool_flag, + format_chapter_count, + format_explicit, + ) + + try: + set_language("zh") + + assert format_bool_flag(1) == "是" + assert format_explicit(1) == "限制级" + assert format_explicit(2) == "洁净版" + assert format_chapter_count(2) == "2 个章节" + finally: + set_language("en") diff --git a/tests/test_mb_list_view.py b/tests/test_mb_list_view.py index 8d7b0ad..a8a9ba7 100644 --- a/tests/test_mb_list_view.py +++ b/tests/test_mb_list_view.py @@ -21,6 +21,7 @@ from GUI.widgets.MBListView import ( COLUMN_CONFIG, DEFAULT_AUDIOBOOK_COLUMNS, + DEFAULT_COLUMNS, DEFAULT_PODCAST_COLUMNS, SORTABLE_NUMERIC_KEYS, MusicBrowserList, @@ -556,6 +557,137 @@ def test_default_column_width_uses_distribution_not_single_outlier(qtbot): assert title_width < outlier_width * 0.6 +def test_default_column_widths_fit_current_viewport(qtbot): + view = _mount_list(qtbot) + view.resize(560, 500) + tracks = [ + { + "Title": f"Long title with extra release notes {idx}", + "Artist": "Very Long Artist Name", + "Album": "Very Long Album Name", + "Genre": "Alternative Rock", + "year": 2001, + "track_number": idx + 1, + "length": 180000, + "rating": 80, + "play_count_1": 5, + "date_added": 1710000000, + } + for idx in range(12) + ] + + _load_content(qtbot, view, tracks=tracks, media_type_filter=0x01) + + header = view.table.horizontalHeader() + assert header is not None + viewport = view.table.viewport() + assert viewport is not None + + assert header.length() <= viewport.width() + 1 + assert view.table.horizontalScrollBar().maximum() == 0 + + +def test_default_columns_do_not_stretch_last_column_to_fill_extra_width(qtbot): + view = _mount_list(qtbot) + view.resize(1400, 500) + tracks = [ + { + "Title": "Yellow", + "Artist": "Coldplay", + "Album": "Parachutes", + "Genre": "Rock", + "year": 2000, + "track_number": 5, + "length": 266000, + "rating": 80, + "play_count_1": 1, + "date_added": 1710000000, + } + ] + + _load_content(qtbot, view, tracks=tracks, media_type_filter=0x01) + + header = view.table.horizontalHeader() + assert header is not None + viewport = view.table.viewport() + assert viewport is not None + last_logical = view.table.columnCount() - 1 + last_natural = view._smart_default_column_width(last_logical) + + assert header.stretchLastSection() is False + assert view.table.columnWidth(last_logical) == last_natural + assert header.length() < viewport.width() + assert view.table.horizontalScrollBar().maximum() == 0 + + +def test_default_column_widths_reexpand_when_viewport_grows(qtbot): + view = _mount_list(qtbot) + view.resize(560, 500) + tracks = [ + { + "Title": f"Long title with extra release notes {idx}", + "Artist": "Very Long Artist Name", + "Album": "Very Long Album Name", + "Genre": "Alternative Rock", + "year": 2001, + "track_number": idx + 1, + "length": 180000, + "rating": 80, + "play_count_1": 5, + "date_added": 1710000000, + } + for idx in range(12) + ] + + _load_content(qtbot, view, tracks=tracks, media_type_filter=0x01) + + title_col = view._columns.index("Title") + narrow_width = view.table.columnWidth(title_col) + + view.resize(1400, 500) + view._fit_current_default_columns_to_viewport() + + header = view.table.horizontalHeader() + assert header is not None + viewport = view.table.viewport() + assert viewport is not None + + assert view.table.columnWidth(title_col) > narrow_width + assert view.table.columnWidth(title_col) == view._smart_default_column_width(title_col) + assert header.length() < viewport.width() + assert view.table.horizontalScrollBar().maximum() == 0 + + +def test_automatic_column_resize_signal_does_not_save_user_layout(qtbot): + view = _mount_list(qtbot) + _load_content(qtbot, view, tracks=_tracks_for_music(), media_type_filter=0x01) + + view.table.setColumnWidth(0, 260) + view._on_header_section_resized(0, 100, 260) + qtbot.wait(100) + + assert ( + view._settings_service + .get_global_settings() + .track_list_columns_by_content + == {} + ) + + +def test_saved_column_widths_are_not_forced_into_viewport(qtbot): + service = _SettingsService() + service.get_global_settings().track_list_columns_by_content = { + "music": {key: 220 for key in DEFAULT_COLUMNS} + } + view = _mount_list(qtbot, settings_service=service) + view.resize(560, 500) + + _load_content(qtbot, view, tracks=_tracks_for_music(), media_type_filter=0x01) + + assert view.table.columnWidth(0) == 220 + assert view.table.horizontalScrollBar().maximum() > 0 + + def test_column_layout_persists_per_content_type(qtbot): view = _mount_list(qtbot) @@ -625,8 +757,10 @@ def test_reset_columns_removes_saved_widths_and_recalculates(qtbot): view = _mount_list(qtbot) _load_content(qtbot, view, tracks=_tracks_for_music(), media_type_filter=0x01) + view._begin_header_interaction() view.table.setColumnWidth(0, 260) view._on_header_section_resized(0, 100, 260) + view._finish_header_interaction() qtbot.waitUntil( lambda: ( view._settings_service.get_global_settings() @@ -741,10 +875,12 @@ def test_column_width_changes_debounced(qtbot): # Simulate multiple resize events (like during a drag) final_artist_width = original_artist_width + view._begin_header_interaction() for i in range(10): final_artist_width = original_artist_width + 10 + i view.table.setColumnWidth(1, final_artist_width) view._on_header_section_resized(1, original_artist_width, final_artist_width) + view._finish_header_interaction() # Wait for debounce timeout to complete qtbot.waitUntil( diff --git a/tests/test_photo_browser.py b/tests/test_photo_browser.py index f4cd5a1..b55d767 100644 --- a/tests/test_photo_browser.py +++ b/tests/test_photo_browser.py @@ -8,7 +8,8 @@ from GUI.styles import context_menu_css from GUI.widgets import photoBrowser as photo_browser_module -from GUI.widgets.photoBrowser import PhotoBrowserWidget +from GUI.widgets.photoBrowser import PhotoBrowserWidget, _album_display_label +from infrastructure.i18n import set_language from SyncEngine.photos import PhotoEntry @@ -88,6 +89,16 @@ def _attach_menu_action_helper(browser: SimpleNamespace) -> None: ) +def test_album_display_label_translates_only_builtin_all_photos() -> None: + try: + set_language("zh") + + assert _album_display_label("All Photos") == "所有照片" + assert _album_display_label("Settings") == "Settings" + finally: + set_language("en") + + def test_sync_running_check_does_not_recurse_before_widget_is_attached() -> None: browser = SimpleNamespace() browser.window = lambda: browser diff --git a/tests/test_playlist_browser.py b/tests/test_playlist_browser.py index 5de9d8f..6f2aef6 100644 --- a/tests/test_playlist_browser.py +++ b/tests/test_playlist_browser.py @@ -7,6 +7,7 @@ _is_ipod_category_playlist, _is_regular_track_playlist, _is_user_smart_playlist, + _playlist_display_title, _podcast_grouping_summary, ) from iTunesDB_Shared.extraction import extract_playlist_item_extras @@ -76,6 +77,32 @@ def test_regular_track_playlist_excludes_generated_playlist_types() -> None: assert not _is_regular_track_playlist({"Title": "Synced", "_source": "sync_playlist_file"}) +def test_playlist_display_title_distinguishes_user_data_from_app_labels() -> None: + assert _playlist_display_title({"Title": "Settings"}) == ("Settings", False) + assert _playlist_display_title({"Title": "Library", "master_flag": 1}) == ( + "Library (Master)", + True, + ) + assert _playlist_display_title({ + "Title": "Music", + "_source": "category", + "_mhsd_dataset_type": 5, + }) == ("Music", True) + assert _playlist_display_title({ + "Title": "音乐", + "_source": "category", + "_mhsd_dataset_type": 5, + "mhsd5_type": 4, + }) == ("Music", True) + assert _playlist_display_title({ + "Title": "视频", + "_source": "smart", + "_mhsd_dataset_type": 5, + "mhsd5_type": 0, + }) == ("Videos", True) + assert _playlist_display_title({}) == ("Untitled", True) + + def test_category_playlist_requires_dataset5_location() -> None: assert _is_ipod_category_playlist({ "Title": "Music", diff --git a/tests/test_settings_page_i18n.py b/tests/test_settings_page_i18n.py new file mode 100644 index 0000000..3db2669 --- /dev/null +++ b/tests/test_settings_page_i18n.py @@ -0,0 +1,131 @@ +from types import SimpleNamespace + +from app_core.services import SettingsSnapshot +from GUI.widgets.settingsPage import SettingsPage +from infrastructure.i18n import get_language, set_language +from infrastructure.settings_schema import AppSettings + + +class _FakeSettingsService: + def __init__(self, settings: AppSettings) -> None: + self.settings = settings + + def get_global_settings(self) -> AppSettings: + return self.settings + + def get_effective_settings(self) -> AppSettings: + return self.settings + + def save_global_settings(self, settings: AppSettings) -> SettingsSnapshot: + self.settings = settings + return SettingsSnapshot.from_settings(settings) + + +class _FakeDeviceSessions: + def current_session(self) -> SimpleNamespace: + return SimpleNamespace( + device_path="", + discovered_ipod=None, + device_settings_loading=False, + ) + + +def test_settings_page_language_selector_saves_and_emits(qtbot) -> None: + try: + set_language("en") + service = _FakeSettingsService(AppSettings()) + page = SettingsPage(service, _FakeDeviceSessions()) + qtbot.addWidget(page) + page.load_from_settings() + + assert page.language_combo.title_label.text() == "Language" + assert [ + page.language_combo.combo.itemText(index) + for index in range(page.language_combo.combo.count()) + ] == ["English", "中文", "Deutsch", "Français", "Español"] + assert page.language_combo.combo.currentText() == "English" + + with qtbot.waitSignal(page.language_changed, timeout=1000): + page.language_combo.combo.setCurrentText("中文") + + assert service.settings.language == "zh" + assert get_language() == "zh" + finally: + set_language("en") + + +def test_settings_page_language_selector_saves_additional_language(qtbot) -> None: + try: + set_language("en") + service = _FakeSettingsService(AppSettings()) + page = SettingsPage(service, _FakeDeviceSessions()) + qtbot.addWidget(page) + page.load_from_settings() + + with qtbot.waitSignal(page.language_changed, timeout=1000): + page.language_combo.combo.setCurrentText("Deutsch") + + assert service.settings.language == "de" + assert get_language() == "de" + finally: + set_language("en") + + +def test_settings_page_storage_labels_use_catalog(qtbot) -> None: + try: + set_language("zh") + service = _FakeSettingsService(AppSettings(language="zh")) + page = SettingsPage(service, _FakeDeviceSessions()) + qtbot.addWidget(page) + page.load_from_settings() + + assert page.transcode_cache_dir.title_label.text() == "缓存位置" + assert page.transcode_cache_dir.path_label.text() == "平台默认" + assert page.max_cache_size.title_label.text() == "最大缓存大小" + assert page.settings_dir.title_label.text() == "设置位置" + assert page.log_dir.title_label.text() == "日志位置" + assert page._section_labels[("Storage", "Locations")].text() == "位置" + finally: + set_language("en") + + +def test_settings_page_combo_rows_translate_display_but_keep_raw_values(qtbot) -> None: + try: + set_language("zh") + service = _FakeSettingsService(AppSettings(language="zh")) + page = SettingsPage(service, _FakeDeviceSessions()) + qtbot.addWidget(page) + page.load_from_settings() + + assert page.sync_workers.combo.currentText() == "自动" + assert page.sync_workers.value == "Auto" + assert page.lossy_quality.combo.currentText() == "均衡" + assert page.lossy_quality.value == "Balanced" + assert page.rating_strategy.combo.currentText() == "iPod 优先" + assert page.rating_strategy.value == "iPod Wins" + + page.backup_before_sync.value = "Ask Each Time" + assert page.backup_before_sync.combo.currentText() == "每次询问" + assert page.backup_before_sync.value == "Ask Each Time" + finally: + set_language("en") + + +def test_settings_page_non_storage_strings_use_catalog(qtbot) -> None: + try: + set_language("zh") + service = _FakeSettingsService(AppSettings(language="zh")) + page = SettingsPage(service, _FakeDeviceSessions()) + qtbot.addWidget(page) + page.load_from_settings() + + assert page.write_back.desc_label.text() == ( + "同步时,将评分和音量均衡值写入电脑音乐文件。关闭时,不会修改电脑文件。" + ) + assert page.ffmpeg_tool.desc_label.text() == ( + "转码和媒体探测所必需。包含 ffmpeg 和 ffprobe。" + ) + assert page.scrobble_on_sync.title_label.text() == "同步时记录播放" + assert page.backup_dir.title_label.text() == "备份位置" + finally: + set_language("en") diff --git a/tests/test_settings_persistence.py b/tests/test_settings_persistence.py index 161d841..893da15 100644 --- a/tests/test_settings_persistence.py +++ b/tests/test_settings_persistence.py @@ -53,6 +53,7 @@ def test_settings_persistence_round_trip(monkeypatch) -> None: track_list_columns_by_content={ "music": {"Title": 220, "Album": 180, "Artist": 160} }, + language="zh", window_width=1440, device_write_workers=2, always_encode_lossy=True, @@ -88,6 +89,7 @@ def test_settings_persistence_round_trip(monkeypatch) -> None: assert loaded.track_list_columns_by_content == { "music": {"Title": 220, "Album": 180, "Artist": 160} } + assert loaded.language == "zh" assert loaded.window_width == 1440 assert loaded.device_write_workers == 2 assert loaded.always_encode_lossy is True @@ -171,3 +173,23 @@ def test_settings_persistence_preserves_legacy_bool_setter(monkeypatch) -> None: assert loaded.backup_before_sync_mode == "ask" assert loaded.backup_before_sync is False + + +def test_settings_persistence_normalizes_language(monkeypatch) -> None: + with repo_temp_dir() as tmp_path: + settings_dir = tmp_path / "settings" + settings_dir.mkdir() + settings_path = settings_dir / "settings.json" + settings_path.write_text( + json.dumps({"language": "zh_CN"}), + encoding="utf-8", + ) + monkeypatch.setattr( + settings_persistence, + "get_settings_path", + lambda: str(settings_path), + ) + + loaded = load_app_settings() + + assert loaded.language == "zh" diff --git a/tests/test_settings_snapshot.py b/tests/test_settings_snapshot.py index 610382d..5984f6d 100644 --- a/tests/test_settings_snapshot.py +++ b/tests/test_settings_snapshot.py @@ -28,6 +28,7 @@ def test_settings_snapshot_copies_values_and_freezes_lists() -> None: track_list_columns_by_content={ "music": {"Title": 240, "Album": 180, "Artist": 160} }, + language="zh", device_write_workers=2, always_encode_lossy=True, convert_wav_to_alac=False, @@ -66,6 +67,7 @@ def test_settings_snapshot_copies_values_and_freezes_lists() -> None: assert snapshot.track_list_columns_by_content == { "music": {"Title": 240, "Album": 180, "Artist": 160} } + assert snapshot.language == "zh" assert snapshot.device_write_workers == 2 assert snapshot.always_encode_lossy is True assert snapshot.convert_wav_to_alac is False diff --git a/tests/test_sidebar_i18n.py b/tests/test_sidebar_i18n.py new file mode 100644 index 0000000..8e31be8 --- /dev/null +++ b/tests/test_sidebar_i18n.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from PyQt6.QtWidgets import QPushButton + +from GUI.widgets.sidebar import Sidebar +from infrastructure.i18n import set_language + + +def _available_button_text_width(button: QPushButton) -> int: + available = button.contentsRect().width() - 6 + if not button.icon().isNull(): + available -= button.iconSize().width() + 6 + return max(0, available) + + +def _button_text_fits(button: QPushButton) -> bool: + return ( + button.fontMetrics().horizontalAdvance(button.text()) + <= _available_button_text_width(button) + 1 + ) + + +def test_sidebar_half_width_actions_fit_french_and_spanish(qtbot) -> None: + try: + for language in ("fr", "es"): + set_language(language) + sidebar = Sidebar() + qtbot.addWidget(sidebar) + sidebar.show() + qtbot.wait(50) + + assert _button_text_fits(sidebar.deviceButton) + assert _button_text_fits(sidebar.rescanButton) + finally: + set_language("en") + + +def test_sidebar_half_width_actions_elide_extreme_labels(qtbot) -> None: + sidebar = Sidebar() + qtbot.addWidget(sidebar) + sidebar.show() + qtbot.wait(50) + + long_label = "Relancer l’analyse complète du périphérique" + sidebar.rescanButton.set_compact_text(long_label) + qtbot.wait(50) + + assert sidebar.rescanButton.toolTip() == long_label + assert sidebar.rescanButton.text() != long_label + assert _button_text_fits(sidebar.rescanButton) diff --git a/tests/test_update_translations.py b/tests/test_update_translations.py new file mode 100644 index 0000000..8e74780 --- /dev/null +++ b/tests/test_update_translations.py @@ -0,0 +1,103 @@ +from pathlib import Path + +from scripts.compile_translations import _read_po +from scripts.update_translations import extract_messages, update_catalog + + +def test_extract_messages_reads_literal_translation_calls(tmp_path: Path) -> None: + source = tmp_path / "sample.py" + source.write_text( + "\n".join( + [ + "from infrastructure.i18n import tr as _", + "label = _('Hello')", + "other = tr('World')", + "dynamic = _(f'Ignored {name}')", + ] + ), + encoding="utf-8", + ) + + messages = extract_messages([source]) + + assert sorted(messages) == ["Hello", "World"] + + +def test_extract_messages_reads_i18n_widget_literals(tmp_path: Path) -> None: + source = tmp_path / "sample.py" + source.write_text( + "\n".join( + [ + "row = ComboRow(", + " 'Mode',", + " 'Choose how work is done.',", + " options=['Auto', 'Manual'],", + " current='Auto',", + ")", + "page = self._make_page('General', 'Appearance', card)", + "self.trackTitleBar.setTitle('All Tracks')", + "self._set_status('Refresh failed')", + "self._show_episode_empty('Waiting', 'No episodes yet.')", + "combo = _make_setting_combo(['Newest Episode', 'Next Episode'])", + ] + ), + encoding="utf-8", + ) + + messages = extract_messages([source]) + + assert sorted(messages) == [ + "All Tracks", + "Appearance", + "Auto", + "Choose how work is done.", + "General", + "Manual", + "Mode", + "Newest Episode", + "Next Episode", + "No episodes yet.", + "Refresh failed", + "Waiting", + ] + + +def test_update_catalog_appends_missing_msgids(tmp_path: Path) -> None: + catalog = tmp_path / "iopenpod.po" + catalog.write_text( + "\n".join( + [ + 'msgid ""', + 'msgstr ""', + '"Language: zh_CN\\n"', + "", + 'msgid "Existing"', + 'msgstr "已有"', + "", + ] + ), + encoding="utf-8", + ) + + missing = update_catalog( + catalog, + {"Existing": ["sample.py:1"], "New String": ["sample.py:2"]}, + check=False, + ) + + assert missing == ["New String"] + assert _read_po(catalog)["New String"] == "" + + +def test_update_catalog_check_reports_missing_without_writing(tmp_path: Path) -> None: + catalog = tmp_path / "iopenpod.po" + catalog.write_text('msgid "Existing"\nmsgstr "已有"\n', encoding="utf-8") + + missing = update_catalog( + catalog, + {"Existing": ["sample.py:1"], "New String": ["sample.py:2"]}, + check=True, + ) + + assert missing == ["New String"] + assert "New String" not in catalog.read_text(encoding="utf-8")