diff --git a/Sora/Localizable.xcstrings b/Sora/Localizable.xcstrings index b4acd89..05a1def 100644 --- a/Sora/Localizable.xcstrings +++ b/Sora/Localizable.xcstrings @@ -8,7 +8,14 @@ }, "%lld Episodes" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Afleveringen" + } + } + } }, "%lld of %lld" : { "localizations" : { @@ -34,25 +41,208 @@ }, "%lld%% seen" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld%% gezien" + } + } + } }, "•" : { }, "About" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "About" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Over" + } + } + } + }, + "About Sora" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "About Sora" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Over Sora" + } + } + } + }, + "Active" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Active" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Actief" + } + } + } + }, + "Active Downloads" : { + }, + "Actively downloading media can be tracked from here." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Actively downloading media can be tracked from here." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Actief downloaden van media kan hier worden gevolgd." + } + } + } }, "Add Module" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add Module" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Module Toevoegen" + } + } + } + }, + "Adjust the number of media items per row in portrait and landscape modes." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Adjust the number of media items per row in portrait and landscape modes." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Pas het aantal media-items per rij aan in staande en liggende modus." + } + } + } + }, + "Advanced" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Advanced" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Geavanceerd" + } + } + } + }, + "AKA Sulfur" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "AKA Sulfur" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "AKA Sulfur" + } + } + } }, "All Bookmarks" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "All Bookmarks" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Alle Bladwijzers" + } + } + } }, "All Prev" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle vorige" + } + } + } }, "All Watching" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "All Watching" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Alles Wat Ik Kijk" + } + } + } + }, + "AniList" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "AniList" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "AniList" + } + } + } }, "Also known as Sulfur" : { @@ -65,154 +255,929 @@ }, "AniList.co" : { + }, + "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Anonieme gegevens worden verzameld om de app te verbeteren. Er worden geen persoonlijke gegevens verzameld. Dit kan op elk moment worden uitgeschakeld." + } + } + } }, "App Data" : { + }, + "App Info" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "App Info" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "App Info" + } + } + } + }, + "App Language" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "App Language" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "App Taal" + } + } + } + }, + "App Storage" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Storage" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Opslag" + } + } + } + }, + "Appearance" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Appearance" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Uiterlijk" + } + } + } }, "Are you sure you want to clear all cached data? This will help free up storage space." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Are you sure you want to clear all cached data? This will help free up storage space." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Weet je zeker dat je alle gecachte gegevens wilt wissen? Dit helpt opslagruimte vrij te maken." + } + } + } }, "Are you sure you want to delete '%@'?" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Are you sure you want to delete '%@'?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Weet je zeker dat je '%@' wilt verwijderen?" + } + } + } + }, + "Are you sure you want to delete all %d episodes in '%@'?" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Are you sure you want to delete all %1$d episodes in '%2$@'?" + } + } + } }, "Are you sure you want to delete all %lld episodes in '%@'?" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { "state" : "new", "value" : "Are you sure you want to delete all %1$lld episodes in '%2$@'?" } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Weet je zeker dat je alle %1$lld afleveringen in '%2$@' wilt verwijderen?" + } } } }, "Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Weet je zeker dat je alle gedownloade bestanden wilt verwijderen? Je kunt ervoor kiezen om alleen de bibliotheek te wissen terwijl je de gedownloade bestanden voor later gebruik bewaart." + } + } + } }, "Are you sure you want to erase all app data? This action cannot be undone." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Are you sure you want to erase all app data? This action cannot be undone." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Weet je zeker dat je alle app-gegevens wilt wissen? Deze actie kan niet ongedaan worden gemaakt." + } + } + } }, "Are you sure you want to remove all downloaded media files (.mov, .mp4, .pkg)? This action cannot be undone." : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet je zeker dat je alle gedownloade mediabestanden (.mov, .mp4, .pkg) wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.\n" + } + } + } }, "Are you sure you want to remove all files in the Documents folder? This will remove all modules." : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet je zeker dat je alle bestanden in de map Documenten wilt verwijderen? Dit zal alle modules verwijderen.\n" + } + } + } }, "Author" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auteur\n" + } + } + } + }, + "Background Enabled" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Background Enabled" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Achtergrond Ingeschakeld" + } + } + } }, "Bookmark items for an easier access later." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bookmark items for an easier access later." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Bladwijzer items voor eenvoudigere toegang later." + } + } + } }, "Bookmarks" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bookmarks" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Bladwijzers" + } + } + } + }, + "Bottom Padding" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bottom Padding" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Onderste Padding" + } + } + } }, "Cancel" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Cancel" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Annuleren" + } + } + } }, "Check out some community modules here!" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Check out some community modules here!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Bekijk hier enkele community modules!" + } + } + } }, "Clear" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Clear" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Wissen" + } + } + } }, "Clear All Downloads" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wis Alle Downloads" + } + } + } }, "Clear Cache" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wis Cache" + } + } + } }, "Clear Library Only" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alleen bibliotheek wissen\n" + } + } + } }, "Clear Logs" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wis Logs" + } + } + } }, "Click the plus button to add a module!" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Click the plus button to add a module!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Klik op de plus-knop om een module toe te voegen!" + } + } + } }, "Continue Watching" : { - - }, - "Copy to Clipboard" : { - - }, - "Copy URL" : { - - }, - "cranci1" : { - - }, - "DATA & LOGS" : { - - }, - "DATA/LOGS" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Data & Logs" + "value" : "Continue Watching" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verder Kijken" + } + } + } + }, + "Continue Watching Episode %d" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Continue Watching Episode %d" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verder Kijken Aflevering %d" + } + } + } + }, + "Contributors" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Contributors" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Bijdragers" + } + } + } + }, + "Copied to Clipboard" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copied to Clipboard" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gekopieerd naar Klembord" + } + } + } + }, + "Copy to Clipboard" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copy to Clipboard" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Kopiëren naar Klembord" + } + } + } + }, + "Copy URL" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Copy URL" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "URL Kopiëren" + } + } + } + }, + "cranci1" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "cranci1" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "cranci1" + } + } + } + }, + "Dark" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Dark" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Donker" + } + } + } + }, + "DATA & LOGS" : { + + }, + "Debug" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Debug" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Debug" + } + } + } + }, + "Debugging and troubleshooting." : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Debugging and troubleshooting." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Debuggen en probleemoplossing." } } } }, "Delete" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Delete" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Verwijderen" + } + } + } }, "Delete All" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alles Wissen" + } + } + } }, "Delete All Downloads" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Downloads Wissen" + } + } + } }, "Delete All Episodes" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Afleveringen Wissen" + } + } + } }, "Delete Download" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Downloads Wissen" + } + } + } }, "Delete Episode" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afleveringen Wissen" + } + } + } + }, + "Double Tap to Seek" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Double Tap to Seek" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Dubbel Tikken om te Zoeken" + } + } + } + }, + "Double tapping the screen on it's sides will skip with the short tap setting." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Double tapping the screen on it's sides will skip with the short tap setting." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Dubbel tikken op de zijkanten van het scherm zal overslaan met de korte tik instelling." + } + } + } }, "Download" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", + "value" : "Download" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Downloaden" + } + } + } + }, + "Download Episode" : { + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aflevering Downloaden" + } + } + } + }, + "Download Summary" : { + + }, + "Downloaded" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Downloaded" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Gedownload" + } + } + } + }, + "Downloaded Shows" : { + + }, + "Downloading" : { + + }, + "Downloads" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Downloads" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", "value" : "Downloads" } } } }, - "Download This Episode" : { - + "Enable Analytics" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Enable Analytics" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Analytics Inschakelen" + } + } + } }, - "Downloads" : { - + "Enable Subtitles" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Enable Subtitles" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Ondertiteling Inschakelen" + } + } + } }, - "Enter the AniList ID for this series" : { - + "Enter the AniList ID for this media" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Enter the AniList ID for this media" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Voer de AniList ID in voor deze media" + } + } + } }, "Episode %lld" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Episode %lld" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Aflevering %lld" + } + } + } }, "Episodes" : { - + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Episodes" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afleveringen" + } + } + } }, "Episodes might not be available yet or there could be an issue with the source." : { - + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Episodes might not be available yet or there could be an issue with the source." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afleveringen zijn mogelijk nog niet beschikbaar of er is een probleem met de bron." + } + } + } + }, + "Episodes Range" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Episodes Range" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Afleveringen Bereik" + } + } + } }, "Erase" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwijden" + } + } + } + }, + "Erase all App Data" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erase all App Data" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wis Alle App Data" + } + } + } }, "Erase App Data" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwijder App Data" + } + } + } }, "Error" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Error" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Fout" + } + } + } }, - "Error Fetching Results" : { - + "Errors and critical issues." : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errors and critical issues." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fouten en kritieke problemen." + } + } + } + }, + "Failed to load contributors" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Failed to load contributors" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Laden van bijdragers mislukt" + } + } + } + }, + "Fetch Episode metadata" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Fetch Episode metadata" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Haal Aflevering Metadata op" + } + } + } }, "Failed to load contributors" : { "localizations" : { @@ -225,24 +1190,266 @@ } }, "Files Downloaded" : { - - }, - "General" : { - - }, - "General Preferences" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { - "state" : "translated", - "value" : "General Settings" + "state" : "new", + "value" : "Files Downloaded" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Gedownloade Bestanden" } } } }, - "INFORMATION" : { - + "Font Size" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Font Size" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Lettergrootte" + } + } + } + }, + "Force Landscape" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Force Landscape" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Forceer Landschap" + } + } + } + }, + "General" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "General" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Algemeen" + } + } + } + }, + "General events and activities." : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "General events and activities." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Algemene gebeurtenissen en activiteiten." + } + } + } + }, + "General Preferences" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "General Preferences" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Algemene Voorkeuren" + } + } + } + }, + "Hide Splash Screen" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Hide Splash Screen" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Splash Screen Verbergen" + } + } + } + }, + "HLS video downloading." : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "HLS video downloading." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "HLS video downloaden." + } + } + } + }, + "Hold Speed" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Hold Speed" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Vasthouden Snelheid" + } + } + } + }, + "Info" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Info" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Info" + } + } + } + }, + "INFOS" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "INFOS" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "INFO" + } + } + } + }, + "Installed Modules" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Installed Modules" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geïnstalleerde Modules" + } + } + } + }, + "Interface" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Interface" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Interface" + } + } + } + }, + "Join the Discord" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join the Discord" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Word lid van de Discord" + } + } + } + }, + "Landscape Columns" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Landscape Columns" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Liggende Kolommen" + } + } + } + }, + "Language" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Language" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Taal" + } + } + } }, "INFOS" : { "extractionState" : "stale", @@ -267,10 +1474,70 @@ } }, "LESS" : { - + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "LESS" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "MINDER" + } + } + } }, "Library" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Library" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Bibliotheek" + } + } + } + }, + "License (GPLv3.0)" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "License (GPLv3.0)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Licentie (GPLv3.0)" + } + } + } + }, + "Light" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Light" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Licht" + } + } + } }, "License (GPLv3.0)" : { "localizations" : { @@ -283,40 +1550,247 @@ } }, "Loading Episode %lld..." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Loading Episode %lld..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Aflevering %lld laden..." + } + } + } }, "Loading logs..." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Loading logs..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Logboeken laden..." + } + } + } }, "Loading module information..." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Loading module information..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Module-informatie laden..." + } + } + } }, "Loading Stream" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Loading Stream" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Stream Laden" + } + } + } }, "Log Debug Info" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Log Debug Info" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Debug Info Loggen" + } + } + } }, "Log Filters" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Log Filters" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Log Filters" + } + } + } }, "Log In with AniList" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Log In with AniList" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Inloggen met AniList" + } + } + } }, "Log In with Trakt" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Log In with Trakt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Inloggen met Trakt" + } + } + } }, "Log Out from AniList" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Log Out from AniList" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Uitloggen van AniList" + } + } + } }, "Log Out from Trakt" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Log Out from Trakt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Uitloggen van Trakt" + } + } + } + }, + "Log Types" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log Types" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Logboek Types" + } + } + } + }, + "Logged in as" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Logged in as" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingelogd als" + } + } + } }, "Logged in as " : { - + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Logged in as " + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Ingelogd als " + } + } + } }, "Logs" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Logs" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Logboeken" + } + } + } + }, + "Long press Skip" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Long press Skip" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Lang Drukken Overslaan" + } + } + } }, "MAIN" : { "extractionState" : "stale", @@ -329,14 +1803,51 @@ } } }, - "MAIN SETTINGS" : { - + "Main Developer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Main Developer" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Hoofdontwikkelaar" + } + } + } + }, + "Mark All Previous Watched" : { + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Markeer alles als gezien\n" + } + } + } }, "Mark as Watched" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Markeer als gezien" + } + } + } }, - "Mark Episode as Watched" : { - + "Mark watched" : { + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Markeer als gezien" + } + } + } }, "Mark Previous Episodes as Watched" : { @@ -354,112 +1865,732 @@ "Match with AniList" : { }, - "Match with TMDB" : { - - }, - "Matched ID: %lld" : { - + "Matched with: %@" : { + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Match met: %@" + } + } + } }, "Max Concurrent Downloads" : { - + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maximaal gelijktijdige downloads\n" + } + } + } }, "me frfr" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "me frfr" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "me frfr" + } + } + } + }, + "Media Grid Layout" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Media Grid Layout" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Media Raster Layout" + } + } + } + }, + "Media Player" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Media Player" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Media Speler" + } + } + } + }, + "Media View" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Media View" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Mediaweergave" + } + } + } + }, + "Metadata Provider" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Metadata Provider" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Metadata Provider" + } + } + } + }, + "Module Removed" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Module Removed" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Module Verwijderd" + } + } + } }, "Modules" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Modules" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Modules" + } + } + } }, "MODULES" : { }, "MORE" : { - + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "MORE" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "MEER" + } + } + } }, "No Active Downloads" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "No Active Downloads" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Geen Actieve Downloads" + } + } + } }, "No AniList matches found" : { }, "No Data Available" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "No Data Available" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Geen Gegevens Beschikbaar" + } + } + } }, "No Downloads" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "No Downloads" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Geen Downloads" + } + } + } }, "No episodes available" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "No episodes available" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Geen afleveringen beschikbaar" + } + } + } }, "No Episodes Available" : { - - }, - "No matches found" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "No matches found. Try different keywords." + "value" : "No Episodes Available" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen Afleveringen Beschikbaar" + } + } + } + }, + "No items to continue watching." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "No items to continue watching." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Geen items om verder te kijken." + } + } + } + }, + "No matches found" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "No matches found" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Geen overeenkomsten gevonden" } } } }, "No Module Selected" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "No Module Selected" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Geen Module Geselecteerd" + } + } + } }, "No Modules" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "No Modules" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Geen Modules" + } + } + } }, - "No Search Results Found" : { - + "No Results Found" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "No Results Found" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Geen Resultaten Gevonden" + } + } + } }, - "Nothing to Continue Watching" : { - + "Note that the modules will be replaced only if there is a different version string inside the JSON file." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Note that the modules will be replaced only if there is a different version string inside the JSON file." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Let op: de modules worden alleen vervangen als er een andere versiestring in het JSON-bestand staat." + } + } + } }, "OK" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "OK" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "OK" + } + } + } }, "Open Community Library" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Community Library" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Community Bibliotheek" + } + } + } }, "Open in AniList" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open in AniList" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Openen in AniList" + } + } + } }, "Original Poster" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Original Poster" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Originele Poster" + } + } + } + }, + "Paused" : { }, "Play" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Play" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Afspelen" + } + } + } }, "Player" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Player" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Speler" + } + } + } + }, + "Please restart the app to apply the language change." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Please restart the app to apply the language change." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Herstart de app om de taalwijziging toe te passen." + } + } + } }, "Please select a module from settings" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Please select a module from settings" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Selecteer een module uit de instellingen" + } + } + } }, - "Provider: %@" : { + "Portrait Columns" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Portrait Columns" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Staande Kolommen" + } + } + } + }, + "Progress bar Marker Color" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Progress bar Marker Color" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Voortgangsbalk Markeerkleur" + } + } + } + }, + "Queue" : { }, "Queued" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Queued" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "In Wachtrij" + } + } + } + }, + "Recently watched content will appear here." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Recently watched content will appear here." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Recent bekeken inhoud verschijnt hier." + } + } + } + }, + "Refresh Modules on Launch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Refresh Modules on Launch" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Ververs Modules bij Opstarten" + } + } + } }, "Refresh Storage Info" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Refresh Storage Info" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Opslaginformatie Vernieuwen" + } + } + } + }, + "Remember Playback speed" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remember Playback speed" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Onthoud Afspeelsnelheid" + } + } + } }, "Remove" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remove" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Verwijderen" + } + } + } + }, + "Remove All Cache" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove All Cache" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwijder Alle Cache" + } + } + } + }, + "Remove All Documents" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove All Documents" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwijder Alle Documenten" + } + } + } }, "Remove Documents" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remove Documents" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Documenten Verwijderen" + } + } + } }, "Remove Downloaded Media" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remove Downloaded Media" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Gedownloade Media Verwijderen" + } + } + } + }, + "Remove Downloads" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove Downloads" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwijder Downloads" + } + } + } }, "Remove from Bookmarks" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remove from Bookmarks" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Verwijderen uit Bladwijzers" + } + } + } }, "Remove Item" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remove Item" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Item Verwijderen" + } + } + } + }, + "Report an Issue" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Report an Issue" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rapporteer een Probleem" + } + } + } }, "Report an Issue" : { "extractionState" : "stale", @@ -473,49 +2604,198 @@ } }, "Reset" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Reset" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Resetten" + } + } + } }, "Reset AniList ID" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Reset AniList ID" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "AniList ID Resetten" + } + } + } }, "Reset Episode Progress" : { }, "Reset progress" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", + "value" : "Reset progress" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voortgang resetten" + } + } + } + }, + "Reset Progress" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", "value" : "Reset Progress" } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Voortgang Resetten" + } + } + } + }, + "Restart Required" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Restart Required" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Herstart Vereist" + } + } + } + }, + "Running Sora %@ - cranci1" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Running Sora %@ - cranci1" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Sora %@ draait - cranci1" + } } } }, "Save" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Save" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Opslaan" + } + } + } }, "Search" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Search" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Zoeken" + } + } + } }, "Search downloads" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Search downloads" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Downloads zoeken" + } + } + } }, "Search for something..." : { "localizations" : { "en" : { "stringUnit" : { - "state" : "translated", + "state" : "new", "value" : "Search for something..." } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Zoek naar iets..." + } } } }, "Search..." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Search..." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Zoeken..." + } + } + } + }, + "Season %d" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Search for something..." + "value" : "Season %d" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seizoen %d" } } } @@ -524,85 +2804,835 @@ }, "Segments Color" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Segments Color" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Segmenten Kleur" + } + } + } }, "Select Module" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Select Module" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Module Selecteren" + } + } + } }, "Set Custom AniList ID" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Set Custom AniList ID" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Aangepaste AniList ID Instellen" + } + } + } }, "Settings" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Settings" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Instellingen" + } + } + } + }, + "Shadow" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Shadow" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Schaduw" + } + } + } }, "Show More (%lld more characters)" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show More (%lld more characters)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Meer Tonen (%lld meer tekens)" + } + } + } + }, + "Show PiP Button" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show PiP Button" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Toon PiP Knop" + } + } + } + }, + "Show Skip 85s Button" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show Skip 85s Button" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Toon Overslaan 85s Knop" + } + } + } + }, + "Show Skip Intro / Outro Buttons" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Show Skip Intro / Outro Buttons" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Toon Overslaan Intro / Outro Knoppen" + } + } + } + }, + "Shows" : { + }, + "Size (%@)" : { + + }, + "Skip Settings" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Skip Settings" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Overslaan Instellingen" + } + } + } + }, + "Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Sommige functies zijn beperkt tot de Sora en Standaard speler, zoals ForceLandscape, holdSpeed en aangepaste tijd overslaan stappen." + } + } + } }, "Sora" : { }, - "Sora %@ by cranci1" : { - + "Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sora en cranci1 zijn op geen enkele manier verbonden met AniList of Trakt.\n\nHoud er ook rekening mee dat voortgangsupdates mogelijk niet 100% nauwkeurig zijn." + } + } + } + }, + "Sora GitHub Repository" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sora GitHub Repository" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sora GitHub Repository" + } + } + } + }, + "Sora/Sulfur will always remain free with no ADs!" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sora/Sulfur will always remain free with no ADs!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Sora/Sulfur blijft altijd gratis zonder advertenties!" + } + } + } }, "Sort" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sort" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Sorteren" + } + } + } + }, + "Speed Settings" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Speed Settings" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Snelheidsinstellingen" + } + } + } + }, + "Start Watching" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start Watching" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start met Kijken" + } + } + } + }, + "Start Watching Episode %d" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start Watching Episode %d" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start met Kijken Aflevering %d" + } + } + } }, "Storage Used" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Storage Used" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Gebruikte Opslag" + } + } + } + }, + "Stream" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stream" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stream" + } + } + } + }, + "Streaming and video playback." : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Streaming and video playback." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Streaming en video afspelen." + } + } + } + }, + "Subtitle Color" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Subtitle Color" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Ondertitelingskleur" + } + } + } + }, + "Subtitle Settings" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Subtitle Settings" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Ondertitelingsinstellingen" + } + } + } + }, + "Sync anime progress" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync anime progress" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchroniseer anime voortgang" + } + } + } + }, + "Sync TV shows progress" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync TV shows progress" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Synchroniseer TV series voortgang" + } + } + } + }, + "System" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "System" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Systeem" + } + } + } }, "Tap a title to override the current match." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Tap a title to override the current match." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Tik op een titel om de huidige match te overschrijven." + } + } + } + }, + "Tap Skip" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Tap Skip" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Tik Overslaan" + } + } + } }, "Tap to manage your modules" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Tap to manage your modules" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Tik om je modules te beheren" + } + } + } }, "Tap to select a module" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Tap to select a module" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Tik om een module te selecteren" + } + } + } + }, + "The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "De app cache helpt de app om afbeeldingen sneller te laden.\n\nHet wissen van de Documents map zal alle gedownloade modules verwijderen.\n\nWis de App Data niet tenzij je de gevolgen begrijpt — het kan ervoor zorgen dat de app niet meer goed werkt." + } + } + } + }, + "The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Het afleveringen bereik bepaalt hoeveel afleveringen er op elke pagina verschijnen. Afleveringen worden gegroepeerd in sets (zoals 1-25, 26-50, enzovoort), waardoor je er gemakkelijker doorheen kunt navigeren.\n\nVoor aflevering metadata verwijst dit naar de aflevering miniatuur en titel, aangezien deze soms spoilers kunnen bevatten." + } + } + } }, "The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." : { - + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "De module heeft slechts één aflevering geleverd, dit is waarschijnlijk een film, daarom hebben we aparte schermen gemaakt voor deze gevallen." + } + } + } + }, + "Thumbnails Width" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Thumbnails Width" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Miniatuur Breedte" + } + } + } }, "TMDB Match" : { }, "Trackers" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Trackers" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Trackers" + } + } + } + }, + "Trakt" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trakt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trakt" + } + } + } }, "Trakt.tv" : { }, - "Try different search terms" : { - + "Try different keywords" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Try different keywords" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Probeer andere zoekwoorden" + } + } + } }, - "Unable to fetch matches. Please try again later." : { - + "Two Finger Hold for Pause" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Two Finger Hold for Pause" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Twee Vingers Vasthouden voor Pauze" + } + } + } }, "Use TMDB Poster Image" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Use TMDB Poster Image" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "TMDB Poster Afbeelding Gebruiken" + } + } + } }, "v%@" : { + }, + "Video Player" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Video Player" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Videospeler" + } + } + } }, "View All" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "View All" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Alles Bekijken" + } + } + } }, "Watched" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Watched" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Bekeken" + } + } + } }, "Why am I not seeing any episodes?" : { - + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Why am I not seeing any episodes?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waarom zie ik geen afleveringen?" + } + } + } + }, + "You are not logged in" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You are not logged in" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je bent niet ingelogd" + } + } + } }, "You have no items saved." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You have no items saved." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Je hebt geen items opgeslagen." + } + } + } }, - "Your active downloads will appear here." : { - + "Your downloaded episodes will appear here" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your downloaded episodes will appear here" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Je gedownloade afleveringen verschijnen hier" + } + } + } }, - "Your downloaded content will appear here" : { - + "Video Quality Preferences" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Video Quality Preferences" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Video Kwaliteit Voorkeuren" + } + } + } }, - "Your recently watched content will appear here" : { - + "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Kies de gewenste videoresolutie voor WiFi en mobiele verbindingen. Hogere resoluties gebruiken meer data maar bieden betere kwaliteit. Als de exacte kwaliteit niet beschikbaar is, wordt automatisch de dichtstbijzijnde optie geselecteerd.\n\nLet op: Niet alle videobronnen en spelers ondersteunen kwaliteitsselectie. Deze functie werkt het beste met HLS-streams met de Sora-speler." + } + } + } + }, + "WiFi Quality" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "WiFi Quality" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "WiFi Kwaliteit" + } + } + } + }, + "Cellular Quality" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Cellular Quality" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "Mobiele Kwaliteit" + } + } + } } }, "version" : "1.0" diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift index ffc5cbc..8093104 100644 --- a/Sora/Views/DownloadView.swift +++ b/Sora/Views/DownloadView.swift @@ -57,16 +57,16 @@ struct DownloadView: View { } .animation(.easeInOut(duration: 0.2), value: selectedTab) .navigationBarHidden(true) - .alert("Delete Download", isPresented: $showDeleteAlert) { - Button("Delete", role: .destructive) { + .alert(NSLocalizedString("Delete Download", comment: ""), isPresented: $showDeleteAlert) { + Button(NSLocalizedString("Delete", comment: ""), role: .destructive) { if let asset = assetToDelete { jsController.deleteAsset(asset) } } - Button("Cancel", role: .cancel) {} + Button(NSLocalizedString("Cancel", comment: ""), role: .cancel) {} } message: { if let asset = assetToDelete { - Text("Are you sure you want to delete '\(asset.episodeDisplayName)'?") + Text(String(format: NSLocalizedString("Are you sure you want to delete '%@'?", comment: ""), asset.episodeDisplayName)) } } } @@ -83,7 +83,7 @@ struct DownloadView: View { VStack(spacing: 20) { if !jsController.downloadQueue.isEmpty { DownloadSectionView( - title: "Queue", + title: NSLocalizedString("Queue", comment: ""), icon: "clock.fill", downloads: jsController.downloadQueue ) @@ -91,7 +91,7 @@ struct DownloadView: View { if !jsController.activeDownloads.isEmpty { DownloadSectionView( - title: "Active Downloads", + title: NSLocalizedString("Active Downloads", comment: ""), icon: "arrow.down.circle.fill", downloads: jsController.activeDownloads ) @@ -140,12 +140,12 @@ struct DownloadView: View { .foregroundStyle(.tertiary) VStack(spacing: 8) { - Text("No Active Downloads") + Text(NSLocalizedString("No Active Downloads", comment: "")) .font(.title2) .fontWeight(.medium) .foregroundStyle(.primary) - Text("Your active downloads will appear here.") + Text(NSLocalizedString("Actively downloading media can be tracked from here.", comment: "")) .font(.subheadline) .foregroundStyle(.secondary) .multilineTextAlignment(.center) @@ -162,12 +162,12 @@ struct DownloadView: View { .foregroundStyle(.tertiary) VStack(spacing: 8) { - Text("No Downloads") + Text(NSLocalizedString("No Downloads", comment: "")) .font(.title2) .fontWeight(.medium) .foregroundStyle(.primary) - Text("Your downloaded content will appear here") + Text(NSLocalizedString("Your downloaded episodes will appear here", comment: "")) .font(.subheadline) .foregroundStyle(.secondary) .multilineTextAlignment(.center) @@ -274,7 +274,7 @@ struct CustomDownloadHeader: View { var body: some View { VStack(spacing: 0) { HStack { - Text("Downloads") + Text(NSLocalizedString("Downloads", comment: "")) .font(.largeTitle) .fontWeight(.bold) .foregroundStyle(.primary) @@ -348,7 +348,7 @@ struct CustomDownloadHeader: View { .frame(width: 18, height: 18) .foregroundColor(.secondary) - TextField("Search downloads", text: $searchText) + TextField(NSLocalizedString("Search downloads", comment: ""), text: $searchText) .textFieldStyle(PlainTextFieldStyle()) .foregroundColor(.primary) @@ -371,16 +371,16 @@ struct CustomDownloadHeader: View { .overlay( RoundedRectangle(cornerRadius: 12) .strokeBorder( - LinearGradient( - gradient: Gradient(stops: [ - .init(color: Color.accentColor.opacity(0.25), location: 0), - .init(color: Color.accentColor.opacity(0), location: 1) - ]), - startPoint: .top, - endPoint: .bottom - ), - lineWidth: 1.5 - ) + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color.accentColor.opacity(0.25), location: 0), + .init(color: Color.accentColor.opacity(0), location: 1) + ]), + startPoint: .top, + endPoint: .bottom + ), + lineWidth: 1.5 + ) ) } .padding(.horizontal, 20) @@ -394,14 +394,14 @@ struct CustomDownloadHeader: View { VStack(spacing: 0) { HStack(spacing: 0) { TabButton( - title: "Active", + title: NSLocalizedString("Active", comment: ""), icon: "arrow.down.circle", isSelected: selectedTab == 0, action: { selectedTab = 0 } ) TabButton( - title: "Downloaded", + title: NSLocalizedString("Downloaded", comment: ""), icon: "checkmark.circle", isSelected: selectedTab == 1, action: { selectedTab = 1 } @@ -526,7 +526,7 @@ struct DownloadSummaryCard: View { HStack { Image(systemName: "chart.bar.fill") .foregroundColor(.accentColor) - Text("Download Summary".uppercased()) + Text(NSLocalizedString("Download Summary", comment: "").uppercased()) .font(.footnote) .fontWeight(.medium) .foregroundColor(.secondary) @@ -538,7 +538,7 @@ struct DownloadSummaryCard: View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 20) { SummaryItem( - title: "Shows", + title: NSLocalizedString("Shows", comment: ""), value: "\(totalShows)", icon: "tv.fill" ) @@ -546,7 +546,7 @@ struct DownloadSummaryCard: View { Divider().frame(height: 32) SummaryItem( - title: "Episodes", + title: NSLocalizedString("Episodes", comment: ""), value: "\(totalEpisodes)", icon: "play.rectangle.fill" ) @@ -559,7 +559,7 @@ struct DownloadSummaryCard: View { let sizeUnit = components.dropFirst().first.map(String.init) ?? "" SummaryItem( - title: "Size (\(sizeUnit))", + title: String(format: NSLocalizedString("Size (%@)", comment: ""), sizeUnit), value: sizeValue, icon: "internaldrive.fill" ) @@ -630,7 +630,7 @@ struct DownloadedSection: View { HStack { Image(systemName: "folder.fill") .foregroundColor(.accentColor) - Text("Downloaded Shows".uppercased()) + Text(NSLocalizedString("Downloaded Shows", comment: "").uppercased()) .font(.footnote) .fontWeight(.medium) .foregroundColor(.secondary) @@ -715,7 +715,7 @@ struct EnhancedActiveDownloadCard: View { VStack(spacing: 6) { HStack { if download.queueStatus == .queued { - Text("Queued") + Text(NSLocalizedString("Queued", comment: "")) .font(.caption) .fontWeight(.medium) .foregroundStyle(.orange) @@ -797,11 +797,11 @@ struct EnhancedActiveDownloadCard: View { private var statusText: String { if download.queueStatus == .queued { - return "Queued" + return NSLocalizedString("Queued", comment: "") } else if taskState == .running { - return "Downloading" + return NSLocalizedString("Downloading", comment: "") } else { - return "Paused" + return NSLocalizedString("Paused", comment: "") } } @@ -1026,7 +1026,7 @@ struct EnhancedShowEpisodesView: View { HStack { Image(systemName: "list.bullet.rectangle") .foregroundColor(.accentColor) - Text("Episodes".uppercased()) + Text(NSLocalizedString("Episodes", comment: "").uppercased()) .font(.footnote) .fontWeight(.medium) .foregroundColor(.secondary) @@ -1051,7 +1051,7 @@ struct EnhancedShowEpisodesView: View { } label: { HStack(spacing: 4) { Image(systemName: episodeSortOption.systemImage) - Text("Sort") + Text(NSLocalizedString("Sort", comment: "")) } .font(.subheadline) .foregroundColor(.accentColor) @@ -1062,7 +1062,7 @@ struct EnhancedShowEpisodesView: View { }) { HStack(spacing: 4) { Image(systemName: "trash") - Text("Delete All") + Text(NSLocalizedString("Delete All", comment: "")) } .font(.subheadline) .foregroundColor(.red) @@ -1073,7 +1073,7 @@ struct EnhancedShowEpisodesView: View { // Episodes List if group.assets.isEmpty { - Text("No episodes available") + Text(NSLocalizedString("No episodes available", comment: "")) .foregroundColor(.secondary) .italic() .padding(40) @@ -1086,7 +1086,7 @@ struct EnhancedShowEpisodesView: View { ) .contextMenu { Button(action: { onPlay(asset) }) { - Label("Play", systemImage: "play.fill") + Label(NSLocalizedString("Play", comment: ""), systemImage: "play.fill") } .disabled(!asset.fileExists) @@ -1094,7 +1094,7 @@ struct EnhancedShowEpisodesView: View { assetToDelete = asset showDeleteAlert = true }) { - Label("Delete", systemImage: "trash") + Label(NSLocalizedString("Delete", comment: ""), systemImage: "trash") } } .onTapGesture { @@ -1107,27 +1107,27 @@ struct EnhancedShowEpisodesView: View { } .padding(.vertical) } - .navigationTitle("Episodes") + .navigationTitle(NSLocalizedString("Episodes", comment: "")) .navigationBarTitleDisplayMode(.inline) - .alert("Delete Episode", isPresented: $showDeleteAlert) { - Button("Cancel", role: .cancel) { } - Button("Delete", role: .destructive) { + .alert(NSLocalizedString("Delete Episode", comment: ""), isPresented: $showDeleteAlert) { + Button(NSLocalizedString("Cancel", comment: ""), role: .cancel) { } + Button(NSLocalizedString("Delete", comment: ""), role: .destructive) { if let asset = assetToDelete { onDelete(asset) } } } message: { if let asset = assetToDelete { - Text("Are you sure you want to delete '\(asset.episodeDisplayName)'?") + Text(String(format: NSLocalizedString("Are you sure you want to delete '%@'?", comment: ""), asset.episodeDisplayName)) } } - .alert("Delete All Episodes", isPresented: $showDeleteAllAlert) { - Button("Cancel", role: .cancel) { } - Button("Delete All", role: .destructive) { + .alert(NSLocalizedString("Delete All Episodes", comment: ""), isPresented: $showDeleteAllAlert) { + Button(NSLocalizedString("Cancel", comment: ""), role: .cancel) { } + Button(NSLocalizedString("Delete All", comment: ""), role: .destructive) { deleteAllAssets() } } message: { - Text("Are you sure you want to delete all \(group.assetCount) episodes in '\(group.title)'?") + Text(String(format: NSLocalizedString("Are you sure you want to delete all %d episodes in '%@'?", comment: ""), group.assetCount, group.title)) } } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index e1c69ad..0fdc24c 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -126,24 +126,33 @@ struct MediaInfoView: View { if episodeLinks.count == 1 { if let _ = unfinished { - return "Continue Watching" + return NSLocalizedString("Continue Watching", comment: "") } - return "Start Watching" + return NSLocalizedString("Start Watching", comment: "") } if let finishedIndex = finished, finishedIndex < episodeLinks.count - 1 { let nextEp = episodeLinks[finishedIndex + 1] - return "Start Watching Episode \(nextEp.number)" + return String(format: NSLocalizedString("Start Watching Episode %d", comment: ""), nextEp.number) } if let unfinishedIndex = unfinished { let currentEp = episodeLinks[unfinishedIndex] - return "Continue Watching Episode \(currentEp.number)" + return String(format: NSLocalizedString("Continue Watching Episode %d", comment: ""), currentEp.number) } - return "Start Watching" + return NSLocalizedString("Start Watching", comment: "") } + private var singleEpisodeWatchText: String { + if let ep = episodeLinks.first { + let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)") + let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)") + let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0 + return progress <= 0.9 ? NSLocalizedString("Mark watched", comment: "") : NSLocalizedString("Reset progress", comment: "") + } + return NSLocalizedString("Mark watched", comment: "") + } var body: some View { ZStack { @@ -336,7 +345,7 @@ struct MediaInfoView: View { .lineLimit(showFullSynopsis ? nil : 3) .animation(nil, value: showFullSynopsis) - Text(showFullSynopsis ? "LESS" : "MORE") + Text(showFullSynopsis ? NSLocalizedString("LESS", comment: "") : NSLocalizedString("MORE", comment: "")) .font(.system(size: 16, weight: .bold)) .foregroundColor(.accentColor) .animation(.easeInOut(duration: 0.3), value: showFullSynopsis) @@ -405,7 +414,7 @@ struct MediaInfoView: View { HStack(spacing: 4) { Image(systemName: "arrow.down.circle") .foregroundColor(.primary) - Text("Download") + Text(NSLocalizedString("Download", comment: "")) .font(.system(size: 14, weight: .medium)) .foregroundColor(.primary) } @@ -420,13 +429,13 @@ struct MediaInfoView: View { } VStack(spacing: 4) { - Text("Why am I not seeing any episodes?") + Text(NSLocalizedString("Why am I not seeing any episodes?", comment: "")) .font(.caption) .bold() .foregroundColor(.gray) .frame(maxWidth: .infinity, alignment: .leading) - Text("The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases.") + Text(NSLocalizedString("The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases.", comment: "")) .font(.caption) .foregroundColor(.gray) .multilineTextAlignment(.leading) @@ -451,16 +460,6 @@ struct MediaInfoView: View { return "checkmark.circle" } - private var singleEpisodeWatchText: String { - if let ep = episodeLinks.first { - let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)") - let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)") - let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0 - return progress <= 0.9 ? "Mark watched" : "Reset progress" - } - return "Mark watched" - } - @ViewBuilder private var episodesSection: some View { if episodeLinks.count != 1 { @@ -474,7 +473,7 @@ struct MediaInfoView: View { @ViewBuilder private var episodesSectionHeader: some View { HStack { - Text("Episodes") + Text(NSLocalizedString("Episodes", comment: "")) .font(.system(size: 22, weight: .bold)) .foregroundColor(.primary) @@ -524,7 +523,7 @@ struct MediaInfoView: View { Menu { ForEach(0.. Int64 { + let fileManager = FileManager.default + var totalSize: Int64 = 0 + let mediaExtensions = [".mov", ".mp4", ".pkg"] + + do { + let contents = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey]) + for url in contents { + let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]) + if resourceValues.isDirectory == true { + totalSize += calculateMediaFilesSize(in: url) } else { - DispatchQueue.main.async { - self.cacheSizeText = "N/A" - self.isCalculatingSize = false - } - } - } - } - - func updateSizes() { - DispatchQueue.global(qos: .background).async { - if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - let size = calculateDirectorySize(for: documentsURL) - DispatchQueue.main.async { - self.documentsSize = size - } - } - } - } - - func calculateDownloadsSize() { - DispatchQueue.global(qos: .background).async { - if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - let size = calculateMediaFilesSize(in: documentsURL) - DispatchQueue.main.async { - self.downloadsSize = size - } - } - } - } - - func calculateMediaFilesSize(in directory: URL) -> Int64 { - let fileManager = FileManager.default - var totalSize: Int64 = 0 - let mediaExtensions = [".mov", ".mp4", ".pkg"] - - do { - let contents = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey]) - for url in contents { - let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]) - if resourceValues.isDirectory == true { - totalSize += calculateMediaFilesSize(in: url) - } else { - let fileExtension = url.pathExtension.lowercased() - if mediaExtensions.contains(".\(fileExtension)") { - totalSize += Int64(resourceValues.fileSize ?? 0) - } - } - } - } catch { - Logger.shared.log("Error calculating media files size: \(error)", type: "Error") - } - - return totalSize - } - - func clearAllCaches() { - clearCache() - } - - func clearCache() { - let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first - do { - if let cacheURL = cacheURL { - let filePaths = try FileManager.default.contentsOfDirectory(at: cacheURL, includingPropertiesForKeys: nil, options: []) - for filePath in filePaths { - try FileManager.default.removeItem(at: filePath) - } - Logger.shared.log("Cache cleared successfully!", type: "General") - calculateCacheSize() - updateSizes() - calculateDownloadsSize() - } - } catch { - Logger.shared.log("Failed to clear cache.", type: "Error") - } - } - - func removeDownloadedMedia() { - let fileManager = FileManager.default - let mediaExtensions = [".mov", ".mp4", ".pkg"] - - if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { - removeMediaFiles(in: documentsURL, extensions: mediaExtensions) - Logger.shared.log("Downloaded media files removed", type: "General") - updateSizes() - calculateDownloadsSize() - } - } - - func removeMediaFiles(in directory: URL, extensions: [String]) { - let fileManager = FileManager.default - - do { - let contents = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.isDirectoryKey]) - for url in contents { - let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey]) - if resourceValues.isDirectory == true { - removeMediaFiles(in: url, extensions: extensions) - } else { - let fileExtension = ".\(url.pathExtension.lowercased())" - if extensions.contains(fileExtension) { - try fileManager.removeItem(at: url) - Logger.shared.log("Removed media file: \(url.lastPathComponent)", type: "General") - } - } - } - } catch { - Logger.shared.log("Error removing media files in \(directory.path): \(error)", type: "Error") - } - } - - func removeAllFilesInDocuments() { - let fileManager = FileManager.default - if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { - do { - let fileURLs = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil) - for fileURL in fileURLs { - try fileManager.removeItem(at: fileURL) - } - Logger.shared.log("All files in documents folder removed", type: "General") - exit(0) - } catch { - Logger.shared.log("Error removing files in documents folder: \(error)", type: "Error") - } - } - } - - func eraseAppData() { - if let domain = Bundle.main.bundleIdentifier { - UserDefaults.standard.removePersistentDomain(forName: domain) - UserDefaults.standard.synchronize() - Logger.shared.log("Cleared app data!", type: "General") - exit(0) - } - } - - func calculateDirectorySize(for url: URL) -> Int64 { - let fileManager = FileManager.default - var totalSize: Int64 = 0 - - do { - let contents = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: [.fileSizeKey]) - for url in contents { - let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]) - if resourceValues.isDirectory == true { - totalSize += calculateDirectorySize(for: url) - } else { + let fileExtension = url.pathExtension.lowercased() + if mediaExtensions.contains(".\(fileExtension)") { totalSize += Int64(resourceValues.fileSize ?? 0) } } - } catch { - Logger.shared.log("Error calculating directory size: \(error)", type: "Error") } - - return totalSize + } catch { + Logger.shared.log("Error calculating media files size: \(error)", type: "Error") } - func formatSize(_ bytes: Int64) -> String { - let formatter = ByteCountFormatter() - formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB] - formatter.countStyle = .file - return formatter.string(fromByteCount: bytes) + return totalSize + } + + func clearAllCaches() { + clearCache() + } + + func clearCache() { + let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first + do { + if let cacheURL = cacheURL { + let filePaths = try FileManager.default.contentsOfDirectory(at: cacheURL, includingPropertiesForKeys: nil, options: []) + for filePath in filePaths { + try FileManager.default.removeItem(at: filePath) + } + Logger.shared.log("Cache cleared successfully!", type: "General") + calculateCacheSize() + updateSizes() + calculateDownloadsSize() + } + } catch { + Logger.shared.log("Failed to clear cache.", type: "Error") } } + + func removeDownloadedMedia() { + let fileManager = FileManager.default + let mediaExtensions = [".mov", ".mp4", ".pkg"] + + if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { + removeMediaFiles(in: documentsURL, extensions: mediaExtensions) + Logger.shared.log("Downloaded media files removed", type: "General") + updateSizes() + calculateDownloadsSize() + } + } + + func removeMediaFiles(in directory: URL, extensions: [String]) { + let fileManager = FileManager.default + + do { + let contents = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.isDirectoryKey]) + for url in contents { + let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey]) + if resourceValues.isDirectory == true { + removeMediaFiles(in: url, extensions: extensions) + } else { + let fileExtension = ".\(url.pathExtension.lowercased())" + if extensions.contains(fileExtension) { + try fileManager.removeItem(at: url) + Logger.shared.log("Removed media file: \(url.lastPathComponent)", type: "General") + } + } + } + } catch { + Logger.shared.log("Error removing media files in \(directory.path): \(error)", type: "Error") + } + } + + func removeAllFilesInDocuments() { + let fileManager = FileManager.default + if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { + do { + let fileURLs = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil) + for fileURL in fileURLs { + try fileManager.removeItem(at: fileURL) + } + Logger.shared.log("All files in documents folder removed", type: "General") + exit(0) + } catch { + Logger.shared.log("Error removing files in documents folder: \(error)", type: "Error") + } + } + } + + func eraseAppData() { + if let domain = Bundle.main.bundleIdentifier { + UserDefaults.standard.removePersistentDomain(forName: domain) + UserDefaults.standard.synchronize() + Logger.shared.log("Cleared app data!", type: "General") + exit(0) + } + } + + func calculateDirectorySize(for url: URL) -> Int64 { + let fileManager = FileManager.default + var totalSize: Int64 = 0 + + do { + let contents = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: [.fileSizeKey]) + for url in contents { + let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]) + if resourceValues.isDirectory == true { + totalSize += calculateDirectorySize(for: url) + } else { + totalSize += Int64(resourceValues.fileSize ?? 0) + } + } + } catch { + Logger.shared.log("Error calculating directory size: \(error)", type: "Error") + } + + return totalSize + } + + func formatSize(_ bytes: Int64) -> String { + let formatter = ByteCountFormatter() + formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB] + formatter.countStyle = .file + return formatter.string(fromByteCount: bytes) + } } + diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index e7b6b61..7663138 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -168,20 +168,21 @@ struct SettingsViewGeneral: View { private let TMDBimageWidhtList = ["300", "500", "780", "1280", "original"] private let sortOrderOptions = ["Ascending", "Descending"] @EnvironmentObject var settings: Settings + @State private var showRestartAlert = false var body: some View { ScrollView { VStack(spacing: 24) { - SettingsSection(title: "Interface") { + SettingsSection(title: NSLocalizedString("Interface", comment: "")) { SettingsPickerRow( icon: "paintbrush", - title: "Appearance", + title: NSLocalizedString("Appearance", comment: ""), options: [Appearance.system, .light, .dark], optionToString: { appearance in switch appearance { - case .system: return "System" - case .light: return "Light" - case .dark: return "Dark" + case .system: return NSLocalizedString("System", comment: "") + case .light: return NSLocalizedString("Light", comment: "") + case .dark: return NSLocalizedString("Dark", comment: "") } }, selection: $settings.selectedAppearance @@ -189,19 +190,32 @@ struct SettingsViewGeneral: View { SettingsToggleRow( icon: "wand.and.rays.inverse", - title: "Hide Splash Screen", + title: NSLocalizedString("Hide Splash Screen", comment: ""), isOn: $hideSplashScreenEnable, showDivider: false ) } + SettingsSection(title: NSLocalizedString("Language", comment: "")) { + SettingsPickerRow( + icon: "globe", + title: NSLocalizedString("App Language", comment: ""), + options: ["English", "Dutch"], + optionToString: { $0 }, + selection: $settings.selectedLanguage + ) + .onChange(of: settings.selectedLanguage) { _ in + showRestartAlert = true + } + } + SettingsSection( - title: "Media View", - footer: "The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." + title: NSLocalizedString("Media View", comment: ""), + footer: NSLocalizedString("The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers.", comment: "") ) { SettingsPickerRow( icon: "list.number", - title: "Episodes Range", + title: NSLocalizedString("Episodes Range", comment: ""), options: [25, 50, 75, 100], optionToString: { "\($0)" }, selection: $episodeChunkSize @@ -209,7 +223,7 @@ struct SettingsViewGeneral: View { SettingsToggleRow( icon: "info.circle", - title: "Fetch Episode metadata", + title: NSLocalizedString("Fetch Episode metadata", comment: ""), isOn: $fetchEpisodeMetadata ) @@ -232,51 +246,91 @@ struct SettingsViewGeneral: View { footer: "Adjust the number of media items per row in portrait and landscape modes." ) { SettingsPickerRow( - icon: "rectangle.portrait", - title: "Portrait Columns", - options: UIDevice.current.userInterfaceIdiom == .pad ? Array(1...5) : Array(1...4), - optionToString: { "\($0)" }, - selection: $mediaColumnsPortrait + icon: "server.rack", + title: NSLocalizedString("Metadata Provider", comment: ""), + options: metadataProvidersList, + optionToString: { $0 }, + selection: $metadataProviders, + showDivider: true ) SettingsPickerRow( - icon: "rectangle", - title: "Landscape Columns", - options: UIDevice.current.userInterfaceIdiom == .pad ? Array(2...8) : Array(2...5), - optionToString: { "\($0)" }, - selection: $mediaColumnsLandscape, + icon: "square.stack.3d.down.right", + title: NSLocalizedString("Thumbnails Width", comment: ""), + options: TMDBimageWidhtList, + optionToString: { $0 }, + selection: $TMDBimageWidht, showDivider: false ) - } - - SettingsSection( - title: "Modules", - footer: "Note that the modules will be replaced only if there is a different version string inside the JSON file." - ) { - SettingsToggleRow( - icon: "arrow.clockwise", - title: "Refresh Modules on Launch", - isOn: $refreshModulesOnLaunch, - showDivider: false - ) - } - - SettingsSection( - title: "Advanced", - footer: "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." - ) { - SettingsToggleRow( - icon: "chart.bar", - title: "Enable Analytics", - isOn: $analyticsEnabled, + } else { + SettingsPickerRow( + icon: "server.rack", + title: NSLocalizedString("Metadata Provider", comment: ""), + options: metadataProvidersList, + optionToString: { $0 }, + selection: $metadataProviders, showDivider: false ) } } - .padding(.vertical, 20) + + SettingsSection( + title: NSLocalizedString("Media Grid Layout", comment: ""), + footer: NSLocalizedString("Adjust the number of media items per row in portrait and landscape modes.", comment: "") + ) { + SettingsPickerRow( + icon: "rectangle.portrait", + title: NSLocalizedString("Portrait Columns", comment: ""), + options: UIDevice.current.userInterfaceIdiom == .pad ? Array(1...5) : Array(1...4), + optionToString: { "\($0)" }, + selection: $mediaColumnsPortrait + ) + + SettingsPickerRow( + icon: "rectangle", + title: NSLocalizedString("Landscape Columns", comment: ""), + options: UIDevice.current.userInterfaceIdiom == .pad ? Array(2...8) : Array(2...5), + optionToString: { "\($0)" }, + selection: $mediaColumnsLandscape, + showDivider: false + ) + } + + SettingsSection( + title: NSLocalizedString("Modules", comment: ""), + footer: NSLocalizedString("Note that the modules will be replaced only if there is a different version string inside the JSON file.", comment: "") + ) { + SettingsToggleRow( + icon: "arrow.clockwise", + title: NSLocalizedString("Refresh Modules on Launch", comment: ""), + isOn: $refreshModulesOnLaunch, + showDivider: false + ) + } + + SettingsSection( + title: NSLocalizedString("Advanced", comment: ""), + footer: NSLocalizedString("Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time.", comment: "") + ) { + SettingsToggleRow( + icon: "chart.bar", + title: NSLocalizedString("Enable Analytics", comment: ""), + isOn: $analyticsEnabled, + showDivider: false + ) + } } .navigationTitle("General") .scrollViewBottomPadding() } + .navigationTitle(NSLocalizedString("General", comment: "")) + .scrollViewBottomPadding() + .alert(isPresented: $showRestartAlert) { + Alert( + title: Text(NSLocalizedString("Restart Required", comment: "")), + message: Text(NSLocalizedString("Please restart the app to apply the language change.", comment: "")), + dismissButton: .default(Text("OK")) + ) + } } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift index c62d5ff..ddb935b 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift @@ -76,12 +76,12 @@ struct SettingsViewLogger: View { var body: some View { ScrollView { VStack(spacing: 24) { - SettingsSection(title: "Logs") { + SettingsSection(title: NSLocalizedString("Logs", comment: "")) { if isLoading { HStack { ProgressView() .scaleEffect(0.8) - Text("Loading logs...") + Text(NSLocalizedString("Loading logs...", comment: "")) .font(.footnote) .foregroundColor(.secondary) } @@ -99,7 +99,7 @@ struct SettingsViewLogger: View { Button(action: { showFullLogs = true }) { - Text("Show More (\(logs.count - displayCharacterLimit) more characters)") + Text(NSLocalizedString("Show More (%lld more characters)", comment: "").replacingOccurrences(of: "%lld", with: "\(logs.count - displayCharacterLimit)")) .font(.footnote) .foregroundColor(.accentColor) } @@ -113,7 +113,7 @@ struct SettingsViewLogger: View { } .padding(.vertical, 20) } - .navigationTitle("Logs") + .navigationTitle(NSLocalizedString("Logs", comment: "")) .onAppear { loadLogsAsync() } @@ -123,14 +123,14 @@ struct SettingsViewLogger: View { Menu { Button(action: { UIPasteboard.general.string = logs - DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) + DropManager.shared.showDrop(title: NSLocalizedString("Copied to Clipboard", comment: ""), subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) }) { - Label("Copy to Clipboard", systemImage: "doc.on.doc") + Label(NSLocalizedString("Copy to Clipboard", comment: ""), systemImage: "doc.on.doc") } Button(role: .destructive, action: { clearLogsAsync() }) { - Label("Clear Logs", systemImage: "trash") + Label(NSLocalizedString("Clear Logs", comment: ""), systemImage: "trash") } } label: { Image(systemName: "ellipsis.circle") diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift index 6588afc..3b6e3c7 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift @@ -115,11 +115,11 @@ class LogFilterViewModel: ObservableObject { private let userDefaultsKey = "LogFilterStates" private let hardcodedFilters: [(type: String, description: String, defaultState: Bool)] = [ - ("General", "General events and activities.", true), - ("Stream", "Streaming and video playback.", true), - ("Error", "Errors and critical issues.", true), - ("Debug", "Debugging and troubleshooting.", false), - ("Download", "HLS video downloading.", true), + (NSLocalizedString("General", comment: ""), NSLocalizedString("General events and activities.", comment: ""), true), + (NSLocalizedString("Stream", comment: ""), NSLocalizedString("Streaming and video playback.", comment: ""), true), + (NSLocalizedString("Error", comment: ""), NSLocalizedString("Errors and critical issues.", comment: ""), true), + (NSLocalizedString("Debug", comment: ""), NSLocalizedString("Debugging and troubleshooting.", comment: ""), false), + (NSLocalizedString("Download", comment: ""), NSLocalizedString("HLS video downloading.", comment: ""), true), ("HTMLStrings", "", false) ] @@ -179,7 +179,7 @@ struct SettingsViewLoggerFilter: View { var body: some View { ScrollView { VStack(spacing: 24) { - SettingsSection(title: "Log Types") { + SettingsSection(title: NSLocalizedString("Log Types", comment: "")) { ForEach($viewModel.filters) { $filter in SettingsToggleRow( icon: iconForFilter(filter.type), @@ -192,6 +192,6 @@ struct SettingsViewLoggerFilter: View { } .padding(.vertical, 20) } - .navigationTitle("Log Filters") + .navigationTitle(NSLocalizedString("Log Filters", comment: "")) } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift index a5611d9..9e03433 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift @@ -119,16 +119,16 @@ fileprivate struct ModuleListItemView: View { .contextMenu { Button(action: { UIPasteboard.general.string = module.metadataUrl - DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) + DropManager.shared.showDrop(title: NSLocalizedString("Copied to Clipboard", comment: ""), subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) }) { - Label("Copy URL", systemImage: "doc.on.doc") + Label(NSLocalizedString("Copy URL", comment: ""), systemImage: "doc.on.doc") } Button(role: .destructive) { if selectedModuleId != module.id.uuidString { onDelete() } } label: { - Label("Delete", systemImage: "trash") + Label(NSLocalizedString("Delete", comment: ""), systemImage: "trash") } .disabled(selectedModuleId == module.id.uuidString) } @@ -137,7 +137,7 @@ fileprivate struct ModuleListItemView: View { Button(role: .destructive) { onDelete() } label: { - Label("Delete", systemImage: "trash") + Label(NSLocalizedString("Delete", comment: ""), systemImage: "trash") } } } @@ -163,25 +163,25 @@ struct SettingsViewModule: View { ScrollView { VStack(spacing: 24) { if moduleManager.modules.isEmpty { - SettingsSection(title: "Modules") { + SettingsSection(title: NSLocalizedString("Modules", comment: "")) { VStack(spacing: 16) { Image(systemName: "plus.app") .font(.largeTitle) .foregroundColor(.secondary) - Text("No Modules") + Text(NSLocalizedString("No Modules", comment: "")) .font(.headline) if didReceiveDefaultPageLink { NavigationLink(destination: CommunityLibraryView() .environmentObject(moduleManager)) { - Text("Check out some community modules here!") + Text(NSLocalizedString("Check out some community modules here!", comment: "")) .font(.caption) .foregroundColor(.accentColor) .frame(maxWidth: .infinity) } .buttonStyle(PlainButtonStyle()) } else { - Text("Click the plus button to add a module!") + Text(NSLocalizedString("Click the plus button to add a module!", comment: "")) .font(.caption) .foregroundColor(.secondary) .frame(maxWidth: .infinity) @@ -191,14 +191,14 @@ struct SettingsViewModule: View { .frame(maxWidth: .infinity) } } else { - SettingsSection(title: "Installed Modules") { + SettingsSection(title: NSLocalizedString("Installed Modules", comment: "")) { ForEach(moduleManager.modules) { module in ModuleListItemView( module: module, selectedModuleId: selectedModuleId, onDelete: { moduleManager.deleteModule(module) - DropManager.shared.showDrop(title: "Module Removed", subtitle: "", duration: 1.0, icon: UIImage(systemName: "trash")) + DropManager.shared.showDrop(title: NSLocalizedString("Module Removed", comment: ""), subtitle: "", duration: 1.0, icon: UIImage(systemName: "trash")) }, onSelect: { selectedModuleId = module.id.uuidString @@ -216,7 +216,7 @@ struct SettingsViewModule: View { .padding(.vertical, 20) } .scrollViewBottomPadding() - .navigationTitle("Modules") + .navigationTitle(NSLocalizedString("Modules", comment: "")) .navigationBarItems(trailing: HStack(spacing: 16) { if didReceiveDefaultPageLink { @@ -228,7 +228,7 @@ struct SettingsViewModule: View { .frame(width: 20, height: 20) .padding(5) } - .accessibilityLabel("Open Community Library") + .accessibilityLabel(NSLocalizedString("Open Community Library", comment: "")) } Button(action: { @@ -239,7 +239,7 @@ struct SettingsViewModule: View { .frame(width: 20, height: 20) .padding(5) } - .accessibilityLabel("Add Module") + .accessibilityLabel(NSLocalizedString("Add Module", comment: "")) } ) .background( diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 6eceb11..e5f182e 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -215,12 +215,12 @@ struct SettingsViewPlayer: View { ScrollView { VStack(spacing: 24) { SettingsSection( - title: "Media Player", - footer: "Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." + title: NSLocalizedString("Media Player", comment: ""), + footer: NSLocalizedString("Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.", comment: "") ) { SettingsPickerRow( icon: "play.circle", - title: "Media Player", + title: NSLocalizedString("Media Player", comment: ""), options: mediaPlayers, optionToString: { $0 }, selection: $externalPlayer @@ -228,35 +228,35 @@ struct SettingsViewPlayer: View { SettingsToggleRow( icon: "rotate.right", - title: "Force Landscape", + title: NSLocalizedString("Force Landscape", comment: ""), isOn: $isAlwaysLandscape ) SettingsToggleRow( icon: "hand.tap", - title: "Two Finger Hold for Pause", + title: NSLocalizedString("Two Finger Hold for Pause", comment: ""), isOn: $holdForPauseEnabled, showDivider: true ) SettingsToggleRow( icon: "pip", - title: "Show PiP Button", + title: NSLocalizedString("Show PiP Button", comment: ""), isOn: $pipButtonVisible, showDivider: false ) } - SettingsSection(title: "Speed Settings") { + SettingsSection(title: NSLocalizedString("Speed Settings", comment: "")) { SettingsToggleRow( icon: "speedometer", - title: "Remember Playback speed", + title: NSLocalizedString("Remember Playback speed", comment: ""), isOn: $isRememberPlaySpeed ) SettingsStepperRow( icon: "forward.fill", - title: "Hold Speed", + title: NSLocalizedString("Hold Speed", comment: ""), value: $holdSpeedPlayer, range: 0.25...2.5, step: 0.25, @@ -264,14 +264,13 @@ struct SettingsViewPlayer: View { showDivider: false ) } - SettingsSection( - title: "Video Quality Preferences", - footer: "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." + title: String(localized: "Video Quality Preferences"), + footer: String(localized: "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player.") ) { SettingsPickerRow( icon: "wifi", - title: "WiFi Quality", + title: String(localized: "WiFi Quality"), options: qualityOptions, optionToString: { $0 }, selection: $wifiQuality @@ -279,7 +278,7 @@ struct SettingsViewPlayer: View { SettingsPickerRow( icon: "antenna.radiowaves.left.and.right", - title: "Cellular Quality", + title: String(localized: "Cellular Quality"), options: qualityOptions, optionToString: { $0 }, selection: $cellularQuality, @@ -287,8 +286,8 @@ struct SettingsViewPlayer: View { ) } - SettingsSection(title: "Progress bar Marker Color") { - ColorPicker("Segments Color", selection: Binding( + SettingsSection(title: NSLocalizedString("Progress bar Marker Color", comment: "")) { + ColorPicker(NSLocalizedString("Segments Color", comment: ""), selection: Binding( get: { if let data = UserDefaults.standard.data(forKey: "segmentsColorData"), let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UIColor { @@ -311,12 +310,12 @@ struct SettingsViewPlayer: View { } SettingsSection( - title: "Skip Settings", - footer: "Double tapping the screen on it's sides will skip with the short tap setting." + title: NSLocalizedString("Skip Settings", comment: ""), + footer: NSLocalizedString("Double tapping the screen on it's sides will skip with the short tap setting.", comment: "") ) { SettingsStepperRow( icon: "goforward", - title: "Tap Skip", + title: NSLocalizedString("Tap Skip", comment: ""), value: $skipIncrement, range: 5...300, step: 5, @@ -325,7 +324,7 @@ struct SettingsViewPlayer: View { SettingsStepperRow( icon: "goforward.plus", - title: "Long press Skip", + title: NSLocalizedString("Long press Skip", comment: ""), value: $skipIncrementHold, range: 5...300, step: 5, @@ -334,19 +333,19 @@ struct SettingsViewPlayer: View { SettingsToggleRow( icon: "hand.tap.fill", - title: "Double Tap to Seek", + title: NSLocalizedString("Double Tap to Seek", comment: ""), isOn: $doubleTapSeekEnabled ) SettingsToggleRow( icon: "forward.end", - title: "Show Skip 85s Button", + title: NSLocalizedString("Show Skip 85s Button", comment: ""), isOn: $skip85Visible ) SettingsToggleRow( icon: "forward.frame", - title: "Show Skip Intro / Outro Buttons", + title: NSLocalizedString("Show Skip Intro / Outro Buttons", comment: ""), isOn: $skipIntroOutroVisible, showDivider: false ) @@ -357,7 +356,7 @@ struct SettingsViewPlayer: View { .padding(.vertical, 20) } .scrollViewBottomPadding() - .navigationTitle("Player") + .navigationTitle(NSLocalizedString("Player", comment: "")) } } @@ -374,10 +373,10 @@ struct SubtitleSettingsSection: View { private let shadowOptions = [0, 1, 3, 6] var body: some View { - SettingsSection(title: "Subtitle Settings") { + SettingsSection(title: NSLocalizedString("Subtitle Settings", comment: "")) { SettingsToggleRow( icon: "captions.bubble", - title: "Enable Subtitles", + title: NSLocalizedString("Enable Subtitles", comment: ""), isOn: $subtitlesEnabled, showDivider: false ) @@ -389,7 +388,7 @@ struct SubtitleSettingsSection: View { SettingsPickerRow( icon: "paintbrush", - title: "Subtitle Color", + title: NSLocalizedString("Subtitle Color", comment: ""), options: colors, optionToString: { $0.capitalized }, selection: $foregroundColor @@ -402,7 +401,7 @@ struct SubtitleSettingsSection: View { SettingsPickerRow( icon: "shadow", - title: "Shadow", + title: NSLocalizedString("Shadow", comment: ""), options: shadowOptions, optionToString: { "\($0)" }, selection: Binding( @@ -418,7 +417,7 @@ struct SubtitleSettingsSection: View { SettingsToggleRow( icon: "rectangle.fill", - title: "Background Enabled", + title: NSLocalizedString("Background Enabled", comment: ""), isOn: $backgroundEnabled ) .onChange(of: backgroundEnabled) { newValue in @@ -429,7 +428,7 @@ struct SubtitleSettingsSection: View { SettingsStepperRow( icon: "textformat.size", - title: "Font Size", + title: NSLocalizedString("Font Size", comment: ""), value: $fontSize, range: 12...36, step: 1 @@ -442,7 +441,7 @@ struct SubtitleSettingsSection: View { SettingsStepperRow( icon: "arrow.up.and.down", - title: "Bottom Padding", + title: NSLocalizedString("Bottom Padding", comment: ""), value: $bottomPadding, range: 0...50, step: 1, diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift index b5fae88..2d4ac59 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift @@ -117,7 +117,7 @@ struct SettingsViewTrackers: View { var body: some View { ScrollView { VStack(spacing: 24) { - SettingsSection(title: "AniList") { + SettingsSection(title: NSLocalizedString("AniList", comment: "")) { VStack(spacing: 0) { HStack(alignment: .center, spacing: 10) { LazyImage(url: URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png")) { state in @@ -137,32 +137,30 @@ struct SettingsViewTrackers: View { } VStack(alignment: .leading, spacing: 4) { - Text("AniList.co") + Text(NSLocalizedString("AniList.co", comment: "")) .font(.title3) .fontWeight(.semibold) - Group { - if isAnilistLoading { - ProgressView() - .scaleEffect(0.8) - .frame(height: 18) - } else if isAnilistLoggedIn { - HStack(spacing: 0) { - Text("Logged in as ") - .font(.footnote) - .foregroundStyle(.gray) - Text(anilistUsername) - .font(.footnote) - .fontWeight(.medium) - .foregroundStyle(profileColor) - } + if isAnilistLoading { + ProgressView() + .scaleEffect(0.8) .frame(height: 18) - } else { - Text(anilistStatus) + } else if isAnilistLoggedIn { + HStack(spacing: 0) { + Text(NSLocalizedString("Logged in as", comment: "")) .font(.footnote) .foregroundStyle(.gray) - .frame(height: 18) + Text(anilistUsername) + .font(.footnote) + .fontWeight(.medium) + .foregroundStyle(profileColor) } + .frame(height: 18) + } else { + Text(NSLocalizedString("You are not logged in", comment: "")) + .font(.footnote) + .foregroundStyle(.gray) + .frame(height: 18) } } .frame(height: 60, alignment: .center) @@ -179,7 +177,7 @@ struct SettingsViewTrackers: View { SettingsToggleRow( icon: "arrow.triangle.2.circlepath", - title: "Sync anime progress", + title: NSLocalizedString("Sync anime progress", comment: ""), isOn: $isSendPushUpdates, showDivider: false ) @@ -200,7 +198,7 @@ struct SettingsViewTrackers: View { .frame(width: 24, height: 24) .foregroundStyle(isAnilistLoggedIn ? .red : .accentColor) - Text(isAnilistLoggedIn ? "Log Out from AniList" : "Log In with AniList") + Text(isAnilistLoggedIn ? NSLocalizedString("Log Out from AniList", comment: "") : NSLocalizedString("Log In with AniList", comment: "")) .foregroundStyle(isAnilistLoggedIn ? .red : .accentColor) Spacer() @@ -212,7 +210,7 @@ struct SettingsViewTrackers: View { } } - SettingsSection(title: "Trakt") { + SettingsSection(title: NSLocalizedString("Trakt", comment: "")) { VStack(spacing: 0) { HStack(alignment: .center, spacing: 10) { LazyImage(url: URL(string: "https://static-00.iconduck.com/assets.00/trakt-icon-2048x2048-2633ksxg.png")) { state in @@ -232,32 +230,30 @@ struct SettingsViewTrackers: View { } VStack(alignment: .leading, spacing: 4) { - Text("Trakt.tv") + Text(NSLocalizedString("Trakt.tv", comment: "")) .font(.title3) .fontWeight(.semibold) - Group { - if isTraktLoading { - ProgressView() - .scaleEffect(0.8) - .frame(height: 18) - } else if isTraktLoggedIn { - HStack(spacing: 0) { - Text("Logged in as ") - .font(.footnote) - .foregroundStyle(.gray) - Text(traktUsername) - .font(.footnote) - .fontWeight(.medium) - .foregroundStyle(.primary) - } + if isTraktLoading { + ProgressView() + .scaleEffect(0.8) .frame(height: 18) - } else { - Text(traktStatus) + } else if isTraktLoggedIn { + HStack(spacing: 0) { + Text(NSLocalizedString("Logged in as", comment: "")) .font(.footnote) .foregroundStyle(.gray) - .frame(height: 18) + Text(traktUsername) + .font(.footnote) + .fontWeight(.medium) + .foregroundStyle(Color.accentColor) } + .frame(height: 18) + } else { + Text(NSLocalizedString("You are not logged in", comment: "")) + .font(.footnote) + .foregroundStyle(.gray) + .frame(height: 18) } } .frame(height: 60, alignment: .center) @@ -268,6 +264,18 @@ struct SettingsViewTrackers: View { .padding(.vertical, 12) .frame(height: 84) + if isTraktLoggedIn { + Divider() + .padding(.horizontal, 16) + + SettingsToggleRow( + icon: "arrow.triangle.2.circlepath", + title: NSLocalizedString("Sync TV shows progress", comment: ""), + isOn: $isSendTraktUpdates, + showDivider: false + ) + } + Divider() .padding(.horizontal, 16) @@ -283,7 +291,7 @@ struct SettingsViewTrackers: View { .frame(width: 24, height: 24) .foregroundStyle(isTraktLoggedIn ? .red : .accentColor) - Text(isTraktLoggedIn ? "Log Out from Trakt" : "Log In with Trakt") + Text(isTraktLoggedIn ? NSLocalizedString("Log Out from Trakt", comment: "") : NSLocalizedString("Log In with Trakt", comment: "")) .foregroundStyle(isTraktLoggedIn ? .red : .accentColor) Spacer() @@ -296,14 +304,14 @@ struct SettingsViewTrackers: View { } SettingsSection( - title: "Info", - footer: "Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." + title: NSLocalizedString("Info", comment: ""), + footer: NSLocalizedString("Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate.", comment: "") ) {} } .padding(.vertical, 20) } .scrollViewBottomPadding() - .navigationTitle("Trackers") + .navigationTitle(NSLocalizedString("Trackers", comment: "")) .onAppear { updateAniListStatus() updateTraktStatus() diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index 30be2ed..51f191c 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -10,13 +10,13 @@ import NukeUI fileprivate struct SettingsNavigationRow: View { let icon: String - let title: String + let titleKey: String let isExternal: Bool let textColor: Color - init(icon: String, title: String, isExternal: Bool = false, textColor: Color = .primary) { + init(icon: String, titleKey: String, isExternal: Bool = false, textColor: Color = .primary) { self.icon = icon - self.title = title + self.titleKey = titleKey self.isExternal = isExternal self.textColor = textColor } @@ -27,7 +27,7 @@ fileprivate struct SettingsNavigationRow: View { .frame(width: 24, height: 24) .foregroundStyle(textColor) - Text(title) + Text(NSLocalizedString(titleKey, comment: "")) .foregroundStyle(textColor) Spacer() @@ -164,22 +164,22 @@ struct SettingsView: View { VStack(spacing: 0) { NavigationLink(destination: SettingsViewGeneral()) { - SettingsNavigationRow(icon: "gearshape", title: "General Settings") + SettingsNavigationRow(icon: "gearshape", titleKey: "General Preferences") } Divider().padding(.horizontal, 16) NavigationLink(destination: SettingsViewPlayer()) { - SettingsNavigationRow(icon: "play.circle", title: "Player Settings") + SettingsNavigationRow(icon: "play.circle", titleKey: "Video Player") } Divider().padding(.horizontal, 16) NavigationLink(destination: SettingsViewDownloads()) { - SettingsNavigationRow(icon: "arrow.down.circle", title: "Download Settings") + SettingsNavigationRow(icon: "arrow.down.circle", titleKey: "Download") } Divider().padding(.horizontal, 16) NavigationLink(destination: SettingsViewTrackers()) { - SettingsNavigationRow(icon: "square.stack.3d.up", title: "Tracking Services") + SettingsNavigationRow(icon: "square.stack.3d.up", titleKey: "Trackers") } } .background(.ultraThinMaterial) @@ -209,12 +209,12 @@ struct SettingsView: View { VStack(spacing: 0) { NavigationLink(destination: SettingsViewData()) { - SettingsNavigationRow(icon: "folder", title: "Data") + SettingsNavigationRow(icon: "folder", titleKey: "Data") } Divider().padding(.horizontal, 16) NavigationLink(destination: SettingsViewLogger()) { - SettingsNavigationRow(icon: "doc.text", title: "Logs") + SettingsNavigationRow(icon: "doc.text", titleKey: "Logs") } } .background(.ultraThinMaterial) @@ -237,21 +237,21 @@ struct SettingsView: View { } VStack(alignment: .leading, spacing: 4) { - Text("INFORMATION") + Text(NSLocalizedString("INFOS", comment: "")) .font(.footnote) .foregroundStyle(.gray) .padding(.horizontal, 20) VStack(spacing: 0) { NavigationLink(destination: SettingsViewAbout()) { - SettingsNavigationRow(icon: "info.circle", title: "About Sora") + SettingsNavigationRow(icon: "info.circle", titleKey: "About Sora") } Divider().padding(.horizontal, 16) Link(destination: URL(string: "https://github.com/cranci1/Sora")!) { SettingsNavigationRow( icon: "chevron.left.forwardslash.chevron.right", - title: "Sora GitHub Repository", + titleKey: "Sora GitHub Repository", isExternal: true, textColor: .gray ) @@ -261,7 +261,7 @@ struct SettingsView: View { Link(destination: URL(string: "https://discord.gg/x7hppDWFDZ")!) { SettingsNavigationRow( icon: "bubble.left.and.bubble.right", - title: "Join Discord Community", + titleKey: "Join the Discord", isExternal: true, textColor: .gray ) @@ -271,7 +271,7 @@ struct SettingsView: View { Link(destination: URL(string: "https://github.com/cranci1/Sora/issues")!) { SettingsNavigationRow( icon: "exclamationmark.circle", - title: "Report an Issue on GitHub", + titleKey: "Report an Issue", isExternal: true, textColor: .gray ) @@ -281,7 +281,7 @@ struct SettingsView: View { Link(destination: URL(string: "https://github.com/cranci1/Sora/blob/dev/LICENSE")!) { SettingsNavigationRow( icon: "doc.text", - title: "License (GPLv3.0)", + titleKey: "License (GPLv3.0)", isExternal: true, textColor: .gray ) @@ -350,6 +350,12 @@ class Settings: ObservableObject { updateAppearance() } } + @Published var selectedLanguage: String { + didSet { + UserDefaults.standard.set(selectedLanguage, forKey: "selectedLanguage") + updateLanguage() + } + } init() { self.accentColor = .primary @@ -359,7 +365,9 @@ class Settings: ObservableObject { } else { self.selectedAppearance = .system } + self.selectedLanguage = UserDefaults.standard.string(forKey: "selectedLanguage") ?? "English" updateAppearance() + updateLanguage() } func updateAccentColor(currentColorScheme: ColorScheme? = nil) { @@ -390,4 +398,10 @@ class Settings: ObservableObject { windowScene.windows.first?.overrideUserInterfaceStyle = .dark } } + + func updateLanguage() { + let languageCode = selectedLanguage == "Dutch" ? "nl" : "en" + UserDefaults.standard.set([languageCode], forKey: "AppleLanguages") + UserDefaults.standard.synchronize() + } }