diff --git a/frontend/src/locales/bg_BG/platform.json b/frontend/src/locales/bg_BG/platform.json index fb49ca52c5..aedacfd493 100644 --- a/frontend/src/locales/bg_BG/platform.json +++ b/frontend/src/locales/bg_BG/platform.json @@ -18,6 +18,7 @@ "export": "Експортирай", "family": "Фамилия", "filter-gallery": "Филтрирай галерията", + "filter-platforms": "Филтрирай платформите", "filters": "Филтри", "firmware-bios": "Фърмуер / BIOS", "firmware-deleted-successfully": "Фърмуерът е изтрит успешно! | {count} фърмуера бяха изтрити успешно!", @@ -34,6 +35,7 @@ "has-soundtrack": "Има саундтрак", "has-states": "Има състояния", "in-library": "В библиотеката", + "include-empty": "Включи празните платформи", "language": "Език", "local-source": "Локален", "match-all-logic": "Отговаря на всички избрани стойности (И)", @@ -44,9 +46,11 @@ "no-firmware-found": "Няма намерен фърмуер", "no-platforms-empty": "Няма намерени платформи. Стартирай сканиране, за да попълниш библиотеката си.", "no-platforms-search": "Няма платформи, отговарящи на \"{query}\".", + "no-platforms-with-games": "Няма платформи с игри. Използвай филтъра, за да включиш празните платформи.", "old-horizontal-cases": "Стари хоризонтални кутии", "old-squared-cases": "Стари квадратни кутии", "on-disk": "На диск", + "only-with-games": "Само платформи с игри", "player-count": "Брой играчи", "random-rom": "Случаен ROM", "random-rom-empty": "Няма ROM в тази платформа", diff --git a/frontend/src/locales/cs_CZ/platform.json b/frontend/src/locales/cs_CZ/platform.json index c4e5626933..da2f694de5 100644 --- a/frontend/src/locales/cs_CZ/platform.json +++ b/frontend/src/locales/cs_CZ/platform.json @@ -18,6 +18,7 @@ "export": "Export", "family": "Rodina", "filter-gallery": "Filtrovat galerii", + "filter-platforms": "Filtrovat platformy", "filters": "Filtry", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "{count} firmware odstraněno! | {count} firmware odstraněn! | {count} firmware odstraněny! | {count} firmware odstraněno!", @@ -34,6 +35,7 @@ "has-soundtrack": "Má zvukový doprovod", "has-states": "Má stavy", "in-library": "V knihovně", + "include-empty": "Zahrnout prázdné platformy", "language": "Jazyk", "local-source": "Lokální", "match-all-logic": "Odpovídá VŠEM vybraným hodnotám (logika A)", @@ -44,9 +46,11 @@ "no-firmware-found": "Nenalezen žádný firmware", "no-platforms-empty": "Nebyly nalezeny žádné platformy. Spusťte skenování pro naplnění knihovny.", "no-platforms-search": "Žádné platformy neodpovídají „{query}“.", + "no-platforms-with-games": "Žádné platformy s hrami. Pomocí filtru zahrňte prázdné platformy.", "old-horizontal-cases": "Staré horizontální obaly", "old-squared-cases": "Staré čtvercové obaly", "on-disk": "Na disku", + "only-with-games": "Pouze platformy s hrami", "player-count": "Počet hráčů", "random-rom": "Náhodné ROM", "random-rom-empty": "Na této platformě nejsou žádné ROM", diff --git a/frontend/src/locales/de_DE/platform.json b/frontend/src/locales/de_DE/platform.json index e1ae424bb9..b6fdcded0e 100644 --- a/frontend/src/locales/de_DE/platform.json +++ b/frontend/src/locales/de_DE/platform.json @@ -18,6 +18,7 @@ "export": "Exportieren", "family": "Familie", "filter-gallery": "Gallerie filtern", + "filter-platforms": "Plattformen filtern", "filters": "Filter", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "Firmware gelöscht! | {count} Firmwares gelöscht!", @@ -34,6 +35,7 @@ "has-soundtrack": "Hat einen Soundtrack", "has-states": "Hat Savestates", "in-library": "In der Bibliothek", + "include-empty": "Leere Plattformen einschließen", "language": "Sprache", "local-source": "Lokal", "match-all-logic": "Entspricht ALLEN ausgewählten Werten (UND-Logik)", @@ -44,9 +46,11 @@ "no-firmware-found": "Keine Firmware gefunden", "no-platforms-empty": "Keine Plattformen gefunden. Führe einen Scan aus, um deine Bibliothek zu füllen.", "no-platforms-search": "Keine Plattformen entsprechen \"{query}\".", + "no-platforms-with-games": "Keine Plattformen mit Spielen. Nutze den Filter, um leere Plattformen einzuschließen.", "old-horizontal-cases": "Alte horizontale Hüllen", "old-squared-cases": "Alte quadratische Hüllen", "on-disk": "Auf Datenträger", + "only-with-games": "Nur Plattformen mit Spielen", "player-count": "Spieleranzahl", "random-rom": "Zufälliges ROM", "random-rom-empty": "Keine ROMs auf dieser Plattform", diff --git a/frontend/src/locales/en_GB/platform.json b/frontend/src/locales/en_GB/platform.json index 4c445d6f5f..a93a70137b 100644 --- a/frontend/src/locales/en_GB/platform.json +++ b/frontend/src/locales/en_GB/platform.json @@ -18,6 +18,7 @@ "export": "Export", "family": "Family", "filter-gallery": "Filter gallery", + "filter-platforms": "Filter platforms", "filters": "Filters", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "Firmware deleted successfully! | {count} firmwares deleted successfully!", @@ -34,6 +35,7 @@ "has-soundtrack": "Has soundtrack", "has-states": "Has save states", "in-library": "In library", + "include-empty": "Include empty platforms", "language": "Language", "local-source": "Local", "match-all-logic": "Match ALL of the selected values (AND logic)", @@ -44,9 +46,11 @@ "no-firmware-found": "No firmware found", "no-platforms-empty": "No platforms found. Run a scan to populate your library.", "no-platforms-search": "No platforms match \"{query}\".", + "no-platforms-with-games": "No platforms with games. Use the filter to include empty platforms.", "old-horizontal-cases": "Old horizontal cases", "old-squared-cases": "Old squared cases", "on-disk": "On disk", + "only-with-games": "Only platforms with games", "player-count": "Player count", "random-rom": "Random ROM", "random-rom-empty": "No ROMs in this platform", diff --git a/frontend/src/locales/en_US/platform.json b/frontend/src/locales/en_US/platform.json index 4c445d6f5f..a93a70137b 100644 --- a/frontend/src/locales/en_US/platform.json +++ b/frontend/src/locales/en_US/platform.json @@ -18,6 +18,7 @@ "export": "Export", "family": "Family", "filter-gallery": "Filter gallery", + "filter-platforms": "Filter platforms", "filters": "Filters", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "Firmware deleted successfully! | {count} firmwares deleted successfully!", @@ -34,6 +35,7 @@ "has-soundtrack": "Has soundtrack", "has-states": "Has save states", "in-library": "In library", + "include-empty": "Include empty platforms", "language": "Language", "local-source": "Local", "match-all-logic": "Match ALL of the selected values (AND logic)", @@ -44,9 +46,11 @@ "no-firmware-found": "No firmware found", "no-platforms-empty": "No platforms found. Run a scan to populate your library.", "no-platforms-search": "No platforms match \"{query}\".", + "no-platforms-with-games": "No platforms with games. Use the filter to include empty platforms.", "old-horizontal-cases": "Old horizontal cases", "old-squared-cases": "Old squared cases", "on-disk": "On disk", + "only-with-games": "Only platforms with games", "player-count": "Player count", "random-rom": "Random ROM", "random-rom-empty": "No ROMs in this platform", diff --git a/frontend/src/locales/es_ES/platform.json b/frontend/src/locales/es_ES/platform.json index 748ffbdd3c..bf12a6cc19 100644 --- a/frontend/src/locales/es_ES/platform.json +++ b/frontend/src/locales/es_ES/platform.json @@ -18,6 +18,7 @@ "export": "Exportar", "family": "Familia", "filter-gallery": "Filtrar galería", + "filter-platforms": "Filtrar plataformas", "filters": "Filtros", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "¡Firmware eliminado! | ¡{count} firmwares eliminados!", @@ -34,6 +35,7 @@ "has-soundtrack": "Tiene banda sonora", "has-states": "Tiene estados guardados", "in-library": "En biblioteca", + "include-empty": "Incluir plataformas vacías", "language": "Idioma", "local-source": "Local", "match-all-logic": "Coincidir con TODOS los valores seleccionados (lógica Y)", @@ -44,9 +46,11 @@ "no-firmware-found": "No se encontraron firmwares", "no-platforms-empty": "No se encontraron plataformas. Ejecuta un escaneo para rellenar tu biblioteca.", "no-platforms-search": "Ninguna plataforma coincide con \"{query}\".", + "no-platforms-with-games": "No hay plataformas con juegos. Usa el filtro para incluir las plataformas vacías.", "old-horizontal-cases": "Cajas horizontales antiguas", "old-squared-cases": "Cajas cuadradas antiguas", "on-disk": "En disco", + "only-with-games": "Solo plataformas con juegos", "player-count": "Número de jugadores", "random-rom": "ROM aleatorio", "random-rom-empty": "No hay ROMs en esta plataforma", diff --git a/frontend/src/locales/fr_FR/platform.json b/frontend/src/locales/fr_FR/platform.json index 4a52f23c2c..fb73f133a4 100644 --- a/frontend/src/locales/fr_FR/platform.json +++ b/frontend/src/locales/fr_FR/platform.json @@ -18,6 +18,7 @@ "export": "Exporter", "family": "Famille", "filter-gallery": "Filtrer la galerie", + "filter-platforms": "Filtrer les plateformes", "filters": "Filtres", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "Firmware supprimé ! | {count} firmwares supprimés !", @@ -34,6 +35,7 @@ "has-soundtrack": "Possède une bande-son", "has-states": "A des états de sauvegarde", "in-library": "Dans la bibliothèque", + "include-empty": "Inclure les plateformes vides", "language": "Langue", "local-source": "Local", "match-all-logic": "Correspond à TOUTES les valeurs sélectionnées (logique ET)", @@ -44,9 +46,11 @@ "no-firmware-found": "Aucun firmware trouvé", "no-platforms-empty": "Aucune plateforme trouvée. Lancez une analyse pour remplir votre bibliothèque.", "no-platforms-search": "Aucune plateforme ne correspond à « {query} ».", + "no-platforms-with-games": "Aucune plateforme avec des jeux. Utilisez le filtre pour inclure les plateformes vides.", "old-horizontal-cases": "Anciens boîtiers horizontaux", "old-squared-cases": "Anciens boîtiers carrés", "on-disk": "Sur le disque", + "only-with-games": "Uniquement les plateformes avec des jeux", "player-count": "Nombre de joueurs", "random-rom": "ROM aléatoire", "random-rom-empty": "Aucune ROM sur cette plateforme", diff --git a/frontend/src/locales/hu_HU/platform.json b/frontend/src/locales/hu_HU/platform.json index c1d2e99607..d5dd52fe97 100644 --- a/frontend/src/locales/hu_HU/platform.json +++ b/frontend/src/locales/hu_HU/platform.json @@ -18,6 +18,7 @@ "export": "Exportálás", "family": "Család", "filter-gallery": "Szűrőgaléria", + "filter-platforms": "Platformok szűrése", "filters": "Szűrők", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "Firmware sikeresen törölve! | {count} firmware sikeresen törölve!", @@ -34,6 +35,7 @@ "has-soundtrack": "Van hozzá zene", "has-states": "Van állásmentése", "in-library": "A könyvtárban", + "include-empty": "Üres platformok megjelenítése", "language": "Nyelv", "local-source": "Helyi", "match-all-logic": "Megfelel MINDEN kiválasztott értéknek (ÉS logika)", @@ -44,9 +46,11 @@ "no-firmware-found": "Firmware nem található", "no-platforms-empty": "Nincsenek platformok. Indíts szkennelést a könyvtár feltöltéséhez.", "no-platforms-search": "Nincs platform a következőre: „{query}”.", + "no-platforms-with-games": "Nincs játékokat tartalmazó platform. A szűrővel megjelenítheted az üres platformokat is.", "old-horizontal-cases": "Régi vízszintes tokok", "old-squared-cases": "Régi négyzetes tokok", "on-disk": "Lemezen", + "only-with-games": "Csak játékokkal rendelkező platformok", "player-count": "Játékosok száma", "random-rom": "Véletlenszerű ROM", "random-rom-empty": "Nincs ROM ezen a platformon", diff --git a/frontend/src/locales/it_IT/platform.json b/frontend/src/locales/it_IT/platform.json index 23f9811770..12e847aa29 100644 --- a/frontend/src/locales/it_IT/platform.json +++ b/frontend/src/locales/it_IT/platform.json @@ -18,6 +18,7 @@ "export": "Esporta", "family": "Famiglia", "filter-gallery": "Filtra galleria", + "filter-platforms": "Filtra piattaforme", "filters": "Filtri", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "Firmware eliminato! | {count} firmware eliminati!", @@ -34,6 +35,7 @@ "has-soundtrack": "Ha una colonna sonora", "has-states": "Ha stati di salvataggio", "in-library": "In libreria", + "include-empty": "Includi piattaforme vuote", "language": "Lingua", "local-source": "Locale", "match-all-logic": "Corrisponde a TUTTI i valori selezionati (logica AND)", @@ -44,9 +46,11 @@ "no-firmware-found": "Nessun firmware trovato", "no-platforms-empty": "Nessuna piattaforma trovata. Esegui una scansione per popolare la tua libreria.", "no-platforms-search": "Nessuna piattaforma corrisponde a \"{query}\".", + "no-platforms-with-games": "Nessuna piattaforma con giochi. Usa il filtro per includere le piattaforme vuote.", "old-horizontal-cases": "Vecchie custodie orizzontali", "old-squared-cases": "Vecchie custodie quadrate", "on-disk": "Su disco", + "only-with-games": "Solo piattaforme con giochi", "player-count": "Numero di giocatori", "random-rom": "ROM casuale", "random-rom-empty": "Nessuna ROM in questa piattaforma", diff --git a/frontend/src/locales/ja_JP/platform.json b/frontend/src/locales/ja_JP/platform.json index a133c1ae8d..c680d9411a 100644 --- a/frontend/src/locales/ja_JP/platform.json +++ b/frontend/src/locales/ja_JP/platform.json @@ -18,6 +18,7 @@ "export": "エクスポート", "family": "ファミリー", "filter-gallery": "ギャラリーをフィルタ", + "filter-platforms": "プラットフォームを絞り込む", "filters": "フィルタ", "firmware-bios": "ファームウェア / BIOS", "firmware-deleted-successfully": "ファームウェアを削除しました! | {count}個のファームウェアを削除しました!", @@ -34,6 +35,7 @@ "has-soundtrack": "サウンドトラックあり", "has-states": "ステートセーブあり", "in-library": "ライブラリ内", + "include-empty": "空のプラットフォームを含める", "language": "言語", "local-source": "ローカル", "match-all-logic": "選択した値のすべてに一致(ANDロジック)", @@ -44,9 +46,11 @@ "no-firmware-found": "ファームウェアが見つかりません", "no-platforms-empty": "プラットフォームが見つかりません。スキャンを実行してライブラリを構築してください。", "no-platforms-search": "「{query}」に一致するプラットフォームはありません。", + "no-platforms-with-games": "ゲームのあるプラットフォームがありません。フィルタで空のプラットフォームを表示できます。", "old-horizontal-cases": "旧横型ケース", "old-squared-cases": "旧角型ケース", "on-disk": "ディスク上", + "only-with-games": "ゲームがあるプラットフォームのみ", "player-count": "プレイヤー", "random-rom": "ランダムROM", "random-rom-empty": "このプラットフォームにROMがありません", diff --git a/frontend/src/locales/ko_KR/platform.json b/frontend/src/locales/ko_KR/platform.json index 3637b0ee25..d0d11bd967 100644 --- a/frontend/src/locales/ko_KR/platform.json +++ b/frontend/src/locales/ko_KR/platform.json @@ -18,6 +18,7 @@ "export": "내보내기", "family": "계열", "filter-gallery": "갤러리 필터", + "filter-platforms": "플랫폼 필터", "filters": "필터", "firmware-bios": "펌웨어 / BIOS", "firmware-deleted-successfully": "펌웨어 삭제 완료! | {count}개 펌웨어 삭제 완료!", @@ -34,6 +35,7 @@ "has-soundtrack": "사운드트랙 있음", "has-states": "상태 저장 있음", "in-library": "라이브러리에 있음", + "include-empty": "빈 플랫폼 포함", "language": "언어", "local-source": "로컬", "match-all-logic": "선택한 모든 값과 일치 (AND 논리)", @@ -44,9 +46,11 @@ "no-firmware-found": "펌웨어 없음", "no-platforms-empty": "플랫폼을 찾을 수 없습니다. 스캔을 실행하여 라이브러리를 채우세요.", "no-platforms-search": "\"{query}\"와(과) 일치하는 플랫폼이 없습니다.", + "no-platforms-with-games": "게임이 있는 플랫폼이 없습니다. 필터를 사용하여 빈 플랫폼을 포함하세요.", "old-horizontal-cases": "옛 가로형 케이스", "old-squared-cases": "옛 정사각형 케이스", "on-disk": "디스크에 있음", + "only-with-games": "게임이 있는 플랫폼만", "player-count": "플레이어 수", "random-rom": "무작위 ROM", "random-rom-empty": "이 플랫폼에 ROM이 없습니다", diff --git a/frontend/src/locales/pl_PL/platform.json b/frontend/src/locales/pl_PL/platform.json index 8db9ae8361..ef4e49395a 100644 --- a/frontend/src/locales/pl_PL/platform.json +++ b/frontend/src/locales/pl_PL/platform.json @@ -18,6 +18,7 @@ "export": "Eksportuj", "family": "Rodzina", "filter-gallery": "Filtruj galerię", + "filter-platforms": "Filtruj platformy", "filters": "Filtry", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "Firmware usunięty! | {count} firmware'ów usuniętych!", @@ -34,6 +35,7 @@ "has-soundtrack": "Ma ścieżkę dźwiękową", "has-states": "Ma stany gry", "in-library": "W bibliotece", + "include-empty": "Uwzględnij puste platformy", "language": "Język", "local-source": "Lokalne", "match-all-logic": "Dopasuj DO WSZYSTKICH wybranych wartości (logika I)", @@ -44,9 +46,11 @@ "no-firmware-found": "Nie znaleziono oprogramowania", "no-platforms-empty": "Nie znaleziono platform. Uruchom skanowanie, aby zapełnić bibliotekę.", "no-platforms-search": "Brak platform pasujących do \"{query}\".", + "no-platforms-with-games": "Brak platform z grami. Użyj filtra, aby uwzględnić puste platformy.", "old-horizontal-cases": "Stare pudełka poziome", "old-squared-cases": "Stare pudełka kwadratowe", "on-disk": "Na dysku", + "only-with-games": "Tylko platformy z grami", "player-count": "Liczba graczy", "random-rom": "Losowy ROM", "random-rom-empty": "Brak ROM-ów na tej platformie", diff --git a/frontend/src/locales/pt_BR/platform.json b/frontend/src/locales/pt_BR/platform.json index 5cdf6c894c..6653c457db 100644 --- a/frontend/src/locales/pt_BR/platform.json +++ b/frontend/src/locales/pt_BR/platform.json @@ -18,6 +18,7 @@ "export": "Exportar", "family": "Família", "filter-gallery": "Filtrar galeria", + "filter-platforms": "Filtrar plataformas", "filters": "Filtros", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "Firmware excluído! | {count} firmwares excluídos!", @@ -34,6 +35,7 @@ "has-soundtrack": "Possui trilha sonora", "has-states": "Tem save states", "in-library": "Na biblioteca", + "include-empty": "Incluir plataformas vazias", "language": "Idioma", "local-source": "Local", "match-all-logic": "Corresponder a TODOS os valores selecionados (lógica E)", @@ -44,9 +46,11 @@ "no-firmware-found": "Nenhum firmware encontrado", "no-platforms-empty": "Nenhuma plataforma encontrada. Execute uma varredura para popular sua biblioteca.", "no-platforms-search": "Nenhuma plataforma corresponde a \"{query}\".", + "no-platforms-with-games": "Nenhuma plataforma com jogos. Use o filtro para incluir as plataformas vazias.", "old-horizontal-cases": "Caixas horizontais antigas", "old-squared-cases": "Caixas quadradas antigas", "on-disk": "No disco", + "only-with-games": "Apenas plataformas com jogos", "player-count": "Niver ar c'hoarierien", "random-rom": "ROM aleatório", "random-rom-empty": "Nenhuma ROM nesta plataforma", diff --git a/frontend/src/locales/ro_RO/platform.json b/frontend/src/locales/ro_RO/platform.json index aa56b62095..397d825e61 100644 --- a/frontend/src/locales/ro_RO/platform.json +++ b/frontend/src/locales/ro_RO/platform.json @@ -18,6 +18,7 @@ "export": "Exportare", "family": "Familie", "filter-gallery": "Filtrare galerie", + "filter-platforms": "Filtrare platforme", "filters": "Filtre", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "Firmware eliminat! | {count} firmware-uri eliminate!", @@ -34,6 +35,7 @@ "has-soundtrack": "Are coloană sonoră", "has-states": "Are stări salvate", "in-library": "În bibliotecă", + "include-empty": "Include platformele goale", "language": "Limba", "local-source": "Local", "match-all-logic": "Corespunde TUTUROR valorilor selectate (logică ȘI)", @@ -44,9 +46,11 @@ "no-firmware-found": "Niciun firmware găsit", "no-platforms-empty": "Nicio platformă găsită. Rulează o scanare pentru a popula biblioteca.", "no-platforms-search": "Nicio platformă nu corespunde căutării „{query}”.", + "no-platforms-with-games": "Nicio platformă cu jocuri. Folosește filtrul pentru a include platformele goale.", "old-horizontal-cases": "Cutii orizontale vechi", "old-squared-cases": "Cutii pătrate vechi", "on-disk": "Pe disc", + "only-with-games": "Doar platformele cu jocuri", "player-count": "Numărul de jucători", "random-rom": "ROM aleatoriu", "random-rom-empty": "Niciun ROM pe această platformă", diff --git a/frontend/src/locales/ru_RU/platform.json b/frontend/src/locales/ru_RU/platform.json index d8ed42a62f..093656c66c 100644 --- a/frontend/src/locales/ru_RU/platform.json +++ b/frontend/src/locales/ru_RU/platform.json @@ -18,6 +18,7 @@ "export": "Экспорт", "family": "Семейство", "filter-gallery": "Фильтр галереи", + "filter-platforms": "Фильтр платформ", "filters": "Фильтры", "firmware-bios": "Прошивка / BIOS", "firmware-deleted-successfully": "Прошивка удалена! | {count} прошивок удалено!", @@ -34,6 +35,7 @@ "has-soundtrack": "Есть саундтрек", "has-states": "Есть состояния", "in-library": "В библиотеке", + "include-empty": "Показывать пустые платформы", "language": "Язык", "local-source": "Локально", "match-all-logic": "Соответствие ВСЕМ выбранным значениям (логика И)", @@ -44,9 +46,11 @@ "no-firmware-found": "Прошивка не найдена", "no-platforms-empty": "Платформы не найдены. Запустите сканирование, чтобы наполнить библиотеку.", "no-platforms-search": "Нет платформ, соответствующих \"{query}\".", + "no-platforms-with-games": "Нет платформ с играми. Используйте фильтр, чтобы показать пустые платформы.", "old-horizontal-cases": "Старые горизонтальные кейсы", "old-squared-cases": "Старые квадратные кейсы", "on-disk": "На диске", + "only-with-games": "Только платформы с играми", "player-count": "Количество игроков", "random-rom": "Случайный ROM", "random-rom-empty": "В этой платформе нет ROM", diff --git a/frontend/src/locales/tr_TR/platform.json b/frontend/src/locales/tr_TR/platform.json index 5a6e7f4529..ed3e99a638 100644 --- a/frontend/src/locales/tr_TR/platform.json +++ b/frontend/src/locales/tr_TR/platform.json @@ -18,6 +18,7 @@ "export": "Dışa aktar", "family": "Aile", "filter-gallery": "Galeriyi filtrele", + "filter-platforms": "Platformları filtrele", "filters": "Filtreler", "firmware-bios": "Firmware / BIOS", "firmware-deleted-successfully": "Firmware başarıyla silindi! | {count} firmware başarıyla silindi!", @@ -34,6 +35,7 @@ "has-soundtrack": "Film müziği var", "has-states": "Durum kayıtları var", "in-library": "Kütüphanede", + "include-empty": "Boş platformları dahil et", "language": "Dil", "local-source": "Yerel", "match-all-logic": "Seçili değerlerin TÜMÜYLE eşleş (VE mantığı)", @@ -44,9 +46,11 @@ "no-firmware-found": "Firmware bulunamadı", "no-platforms-empty": "Platform bulunamadı. Kütüphanenizi doldurmak için tarama başlatın.", "no-platforms-search": "\"{query}\" ile eşleşen platform bulunamadı.", + "no-platforms-with-games": "Oyunu olan platform yok. Boş platformları dahil etmek için filtreyi kullanın.", "old-horizontal-cases": "Eski yatay kutular", "old-squared-cases": "Eski kare kutular", "on-disk": "Diskte", + "only-with-games": "Yalnızca oyunu olan platformlar", "player-count": "Oyuncu sayısı", "random-rom": "Rastgele ROM", "random-rom-empty": "Bu platformda ROM yok", diff --git a/frontend/src/locales/zh_CN/platform.json b/frontend/src/locales/zh_CN/platform.json index f73ad27566..3900424838 100644 --- a/frontend/src/locales/zh_CN/platform.json +++ b/frontend/src/locales/zh_CN/platform.json @@ -18,6 +18,7 @@ "export": "导出", "family": "家族", "filter-gallery": "筛选游戏库", + "filter-platforms": "筛选平台", "filters": "筛选器", "firmware-bios": "固件 / BIOS", "firmware-deleted-successfully": "固件已删除! | 已删除{count}个固件!", @@ -34,6 +35,7 @@ "has-soundtrack": "有原声带", "has-states": "有即时存档", "in-library": "在游戏库中", + "include-empty": "包含空平台", "language": "语言", "local-source": "本地", "match-all-logic": "匹配所有选定值(与逻辑)", @@ -44,9 +46,11 @@ "no-firmware-found": "没有找到固件", "no-platforms-empty": "未找到平台。运行扫描以填充您的游戏库。", "no-platforms-search": "没有符合 \"{query}\" 的平台。", + "no-platforms-with-games": "没有包含游戏的平台。使用筛选器可显示空平台。", "old-horizontal-cases": "旧式横向盒装", "old-squared-cases": "旧式方形盒装", "on-disk": "磁盘上", + "only-with-games": "仅显示有游戏的平台", "player-count": "玩家人数", "random-rom": "随机 ROM", "random-rom-empty": "此平台没有 ROM", diff --git a/frontend/src/locales/zh_TW/platform.json b/frontend/src/locales/zh_TW/platform.json index e988d30d4c..9aff61b7b0 100644 --- a/frontend/src/locales/zh_TW/platform.json +++ b/frontend/src/locales/zh_TW/platform.json @@ -18,6 +18,7 @@ "export": "導出", "family": "家族", "filter-gallery": "篩選遊戲庫", + "filter-platforms": "篩選平台", "filters": "篩選器", "firmware-bios": "韌體 / BIOS", "firmware-deleted-successfully": "韌體已刪除! | 已刪除{count}個韌體!", @@ -34,6 +35,7 @@ "has-soundtrack": "有原聲帶", "has-states": "有即時存檔", "in-library": "已在遊戲庫", + "include-empty": "包含空平台", "language": "語言", "local-source": "本地", "match-all-logic": "匹配所有選定值(與邏輯)", @@ -44,9 +46,11 @@ "no-firmware-found": "没有找到韌體", "no-platforms-empty": "找不到任何平台。執行掃描以填充您的遊戲庫。", "no-platforms-search": "找不到符合「{query}」的平台。", + "no-platforms-with-games": "沒有包含遊戲的平台。使用篩選器可顯示空平台。", "old-horizontal-cases": "舊式橫向卡帶盒", "old-squared-cases": "舊式方形卡帶盒", "on-disk": "在磁碟上", + "only-with-games": "僅顯示有遊戲的平台", "player-count": "玩家人數", "random-rom": "隨機 ROM", "random-rom-empty": "此平台沒有 ROM", diff --git a/frontend/src/v2/composables/useGalleryViewModeUrl/index.ts b/frontend/src/v2/composables/useGalleryViewModeUrl/index.ts index 64f0653c1f..511021e3a7 100644 --- a/frontend/src/v2/composables/useGalleryViewModeUrl/index.ts +++ b/frontend/src/v2/composables/useGalleryViewModeUrl/index.ts @@ -26,6 +26,7 @@ import { type LayoutMode, useGalleryMode, } from "@/v2/composables/useGalleryMode"; +import { patchQuery } from "@/v2/utils/routeQuery"; const VALID_GROUP_BY: readonly GroupByMode[] = [ "letter", @@ -72,10 +73,7 @@ export function useGalleryViewModeUrl() { ? (route.query[key] as string) : undefined; if (value === current) return; - const nextQuery = { ...route.query }; - if (value === undefined) delete nextQuery[key]; - else nextQuery[key] = value; - router.replace({ query: nextQuery }); + patchQuery(router, { [key]: value }); } // Drop the param when the value is the default — keeps URLs clean. diff --git a/frontend/src/v2/composables/useTileSearchUrl/index.ts b/frontend/src/v2/composables/useTileSearchUrl/index.ts index a65d86f2ea..45fc4f0f5c 100644 --- a/frontend/src/v2/composables/useTileSearchUrl/index.ts +++ b/frontend/src/v2/composables/useTileSearchUrl/index.ts @@ -18,6 +18,7 @@ // keystroke). Skips the push when the URL already matches. import { ref, watch, type Ref } from "vue"; import { useRoute, useRouter } from "vue-router"; +import { patchQuery } from "@/v2/utils/routeQuery"; export function useTileSearchUrl(): Ref { const route = useRoute(); @@ -40,10 +41,7 @@ export function useTileSearchUrl(): Ref { const current = typeof route.query.search === "string" ? route.query.search : undefined; if (desired === current) return; - const nextQuery = { ...route.query }; - if (desired === undefined) delete nextQuery.search; - else nextQuery.search = desired; - router.replace({ query: nextQuery }); + patchQuery(router, { search: desired }); }); return term; diff --git a/frontend/src/v2/utils/routeQuery.test.ts b/frontend/src/v2/utils/routeQuery.test.ts new file mode 100644 index 0000000000..f0dae04125 --- /dev/null +++ b/frontend/src/v2/utils/routeQuery.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it, vi } from "vitest"; +import { nextTick, ref } from "vue"; +import type { LocationQuery } from "vue-router"; +import { patchQuery, type QueryRouter } from "./routeQuery"; + +// Minimal router stub reproducing the trait that causes the bug: `replace` +// is async, so `currentRoute.query` still reads the pre-navigation value +// for anything running in the same flush. +function fakeRouter(initial: LocationQuery = {}) { + const currentRoute = ref({ query: { ...initial } }); + const replace = vi.fn(async ({ query }: { query: LocationQuery }) => { + await Promise.resolve(); + currentRoute.value = { query: { ...query } }; + }); + const router: QueryRouter & { replace: typeof replace } = { + currentRoute, + replace, + }; + return router; +} + +describe("patchQuery", () => { + it("merges same-tick writes into one navigation", async () => { + const router = fakeRouter({ search: "zelda" }); + + patchQuery(router, { show: "all" }); + patchQuery(router, { layout: "list" }); + await nextTick(); + + expect(router.replace).toHaveBeenCalledTimes(1); + expect(router.replace).toHaveBeenCalledWith({ + query: { search: "zelda", show: "all", layout: "list" }, + }); + }); + + it("keeps a param a later same-tick write didn't touch", async () => { + const router = fakeRouter({ vis: "private" }); + + // The exact shape of the reported bug: one control sets a param while + // another clears a different one in the same flush. + patchQuery(router, { kind: "virtual" }); + patchQuery(router, { vis: undefined }); + await nextTick(); + + expect(router.replace).toHaveBeenCalledWith({ + query: { kind: "virtual" }, + }); + }); + + it("drops a param when the patch value is undefined", async () => { + const router = fakeRouter({ search: "zelda", show: "all" }); + + patchQuery(router, { search: undefined }); + await nextTick(); + + expect(router.replace).toHaveBeenCalledWith({ query: { show: "all" } }); + }); + + it("builds later ticks off the navigated query", async () => { + const router = fakeRouter(); + + patchQuery(router, { show: "all" }); + await nextTick(); + await Promise.resolve(); + + patchQuery(router, { search: "zelda" }); + await nextTick(); + + expect(router.replace).toHaveBeenCalledTimes(2); + expect(router.replace).toHaveBeenLastCalledWith({ + query: { show: "all", search: "zelda" }, + }); + }); +}); diff --git a/frontend/src/v2/utils/routeQuery.ts b/frontend/src/v2/utils/routeQuery.ts new file mode 100644 index 0000000000..28640082eb --- /dev/null +++ b/frontend/src/v2/utils/routeQuery.ts @@ -0,0 +1,47 @@ +// Batched query-param writes for URL-backed view state (search, view mode, +// toolbar filters). +// +// `router.replace` is async: `route.query` keeps returning the old value +// until the navigation resolves. Two controls changing in the same flush +// therefore both read the same stale snapshot, and the second `replace` +// wins with a query that never saw the first one's key, silently resetting +// it. Routing every writer through here merges the patches and issues one +// navigation per tick instead. +import { nextTick } from "vue"; +import type { LocationQuery } from "vue-router"; + +/** The slice of vue-router's `Router` this needs. A real Router satisfies + * it structurally; narrowing it keeps the util testable without a stub + * that has to impersonate the whole router. */ +export interface QueryRouter { + readonly currentRoute: { value: { query: LocationQuery } }; + replace(to: { query: LocationQuery }): unknown; +} + +// Pending merge for the current tick, keyed by router so two router +// instances (tests, nested apps) never share a buffer. +const pendingByRouter = new WeakMap(); + +/** Merge `patch` into the current query; `undefined` drops the param. */ +export function patchQuery( + router: QueryRouter, + patch: Record, +): void { + const merged: LocationQuery = pendingByRouter.get(router) ?? { + ...router.currentRoute.value.query, + }; + + for (const [key, value] of Object.entries(patch)) { + if (value === undefined) delete merged[key]; + else merged[key] = value; + } + + pendingByRouter.set(router, merged); + + void nextTick(() => { + const query = pendingByRouter.get(router); + if (!query) return; + pendingByRouter.delete(router); + void router.replace({ query }); + }); +} diff --git a/frontend/src/v2/views/CollectionsIndex.vue b/frontend/src/v2/views/CollectionsIndex.vue index fecc25657e..d36d1f5a4e 100644 --- a/frontend/src/v2/views/CollectionsIndex.vue +++ b/frontend/src/v2/views/CollectionsIndex.vue @@ -39,6 +39,7 @@ import { useTileSearchUrl } from "@/v2/composables/useTileSearchUrl"; import { useWebpSupport } from "@/v2/composables/useWebpSupport"; import { useWrapGridNav } from "@/v2/composables/useWrapGridNav"; import { collectionCoverList } from "@/v2/utils/collectionCovers"; +import { patchQuery } from "@/v2/utils/routeQuery"; type AnyCollection = Collection | VirtualCollection | SmartCollection; type Kind = "regular" | "virtual" | "smart"; @@ -128,10 +129,7 @@ function writeEnumQuery(param: string, value: string, fallback: string) { const current = route.query[param]; const currentStr = typeof current === "string" ? current : fallback; if ((desired ?? fallback) === currentStr) return; - const nextQuery = { ...route.query }; - if (desired === undefined) delete nextQuery[param]; - else nextQuery[param] = desired; - router.replace({ query: nextQuery }); + patchQuery(router, { [param]: desired }); } const kindFilter = ref( diff --git a/frontend/src/v2/views/PlatformsIndex.test.ts b/frontend/src/v2/views/PlatformsIndex.test.ts index 3eb467a10a..ac499bdb2a 100644 --- a/frontend/src/v2/views/PlatformsIndex.test.ts +++ b/frontend/src/v2/views/PlatformsIndex.test.ts @@ -10,6 +10,18 @@ vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }), })); +// Plain object rather than a reactive route: every test sets the query +// before mounting, which is when the view reads it. +const { routeState } = vi.hoisted(() => ({ + routeState: { query: {} as Record }, +})); + +vi.mock("vue-router", async (importOriginal) => ({ + ...(await importOriginal()), + useRoute: () => routeState, + useRouter: () => ({ replace: vi.fn() }), +})); + vi.mock("@v2/lib", () => ({ RDivider: defineComponent({ template: "
" }), RLetterHeading: defineComponent({ @@ -112,9 +124,22 @@ function platform(id: number, displayName: string, romCount: number): Platform { describe("PlatformsIndex", () => { beforeEach(() => { setActivePinia(createPinia()); + routeState.query = {}; + }); + + it("hides empty platforms by default", () => { + const platforms = storePlatforms(); + platforms.set([platform(2, "Nintendo 64", 12), platform(1, "Game Boy", 0)]); + + const wrapper = mount(PlatformsIndex); + + expect(wrapper.text()).toContain("common.platforms 1"); + expect(wrapper.text()).toContain("Nintendo 64 12"); + expect(wrapper.text()).not.toContain("Game Boy 0"); }); - it("renders empty database platforms so they remain reachable", () => { + it("renders empty database platforms when ?show=all, so they stay reachable", () => { + routeState.query = { show: "all" }; const platforms = storePlatforms(); platforms.set([platform(2, "Nintendo 64", 12), platform(1, "Game Boy", 0)]); @@ -124,4 +149,13 @@ describe("PlatformsIndex", () => { expect(wrapper.text()).toContain("Game Boy 0"); expect(wrapper.text()).toContain("Nintendo 64 12"); }); + + it("points at the filter when every platform is empty", () => { + const platforms = storePlatforms(); + platforms.set([platform(1, "Game Boy", 0)]); + + const wrapper = mount(PlatformsIndex); + + expect(wrapper.text()).toContain("platform.no-platforms-with-games"); + }); }); diff --git a/frontend/src/v2/views/PlatformsIndex.vue b/frontend/src/v2/views/PlatformsIndex.vue index 5ceb2796e8..1a975b4635 100644 --- a/frontend/src/v2/views/PlatformsIndex.vue +++ b/frontend/src/v2/views/PlatformsIndex.vue @@ -1,5 +1,5 @@