diff options
60 files changed, 946 insertions, 744 deletions
diff --git a/assets/css/default.css b/assets/css/default.css index fd696178..a47762ec 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -198,6 +198,7 @@ img.thumbnail { display: block; /* See: https://stackoverflow.com/a/11635197 */ width: 100%; object-fit: cover; + aspect-ratio: 16 / 9; } .thumbnail-placeholder { diff --git a/assets/js/notifications.js b/assets/js/notifications.js index 058553d9..55b7a15c 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -10,7 +10,7 @@ var notifications, delivered; var notifications_mock = { close: function () { } }; function get_subscriptions() { - helpers.xhr('GET', '/api/v1/auth/subscriptions?fields=authorId', { + helpers.xhr('GET', '/api/v1/auth/subscriptions', { retries: 5, entity_name: 'subscriptions' }, { @@ -22,7 +22,7 @@ function create_notification_stream(subscriptions) { // sse.js can't be replaced to EventSource in place as it lack support of payload and headers // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource notifications = new SSE( - '/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', { + '/api/v1/auth/notifications', { withCredentials: true, payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId; }).join(','), headers: { 'Content-Type': 'application/x-www-form-urlencoded' } diff --git a/docker-compose.yml b/docker-compose.yml index 42a5c06b..7e33f6e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,9 +36,6 @@ services: interval: 30s timeout: 5s retries: 2 - depends_on: - invidious-db: - condition: service_healthy invidious-db: image: docker.io/library/postgres:14 diff --git a/locales/ar.json b/locales/ar.json index 18298913..57062e89 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -41,7 +41,7 @@ "Time (h:mm:ss):": "الوقت (h:mm:ss):", "Text CAPTCHA": "نص الكابتشا", "Image CAPTCHA": "صورة الكابتشا", - "Sign In": "تسجيل الدخول", + "Sign In": "إنشاء حساب", "Register": "التسجيل", "E-mail": "البريد الإلكتروني", "Preferences": "الإعدادات", @@ -554,5 +554,7 @@ "generic_channels_count_2": "{{count}} قناتان", "generic_channels_count_3": "{{count}} قنوات", "generic_channels_count_4": "{{count}} قنوات", - "generic_channels_count_5": "{{count}} قناة" + "generic_channels_count_5": "{{count}} قناة", + "Import YouTube watch history (.json)": "استيراد سجل مشاهدة YouTube بصيغة (.json)", + "toggle_theme": "تبديل الموضوع" } diff --git a/locales/bg.json b/locales/bg.json index 82591ed8..bcce6a7a 100644 --- a/locales/bg.json +++ b/locales/bg.json @@ -486,5 +486,6 @@ "preferences_annotations_label": "Покажи анотаций по подразбиране: ", "generic_views_count": "{{count}} гледане", "generic_views_count_plural": "{{count}} гледания", - "Next page": "Следваща страница" + "Next page": "Следваща страница", + "Import YouTube watch history (.json)": "Импортиране на историята на гледане от YouTube (.json)" } diff --git a/locales/ca.json b/locales/ca.json index a718eb2b..4ae55804 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -486,5 +486,6 @@ "generic_channels_count_plural": "{{count}} canals", "generic_button_edit": "Edita", "generic_button_rss": "RSS", - "generic_button_delete": "Suprimeix" + "generic_button_delete": "Suprimeix", + "Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)" } diff --git a/locales/cs.json b/locales/cs.json index 10c114eb..4aa20f28 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -503,5 +503,7 @@ "playlist_button_add_items": "Přidat videa", "generic_channels_count_0": "{{count}} kanál", "generic_channels_count_1": "{{count}} kanály", - "generic_channels_count_2": "{{count}} kanálů" + "generic_channels_count_2": "{{count}} kanálů", + "Import YouTube watch history (.json)": "Importovat historii sledování z YouTube (.json)", + "toggle_theme": "Přepnout motiv" } diff --git a/locales/da.json b/locales/da.json index 16607546..019f1c51 100644 --- a/locales/da.json +++ b/locales/da.json @@ -452,5 +452,40 @@ "crash_page_you_found_a_bug": "Det ser ud til, at du har fundet en fejl i Invidious!", "crash_page_read_the_faq": "læs <a href=\"`x`\">Ofte stillede spørgsmål (FAQ)</a>", "crash_page_search_issue": "søgte efter <a href=\"`x`\">eksisterende problemer på GitHub</a>", - "search_filters_title": "Filter" + "search_filters_title": "Filter", + "playlist_button_add_items": "Tilføj videoer", + "search_message_no_results": "Ingen resultater fundet.", + "Import YouTube watch history (.json)": "Importer YouTube afspilningshistorik (.json)", + "search_message_change_filters_or_query": "Prøv at udvide din søgeforspørgsel og/eller ændre filtrene.", + "search_message_use_another_instance": " Du kan også <a href=\"`x`\">søge på en anden instans</a>.", + "Music in this video": "Musik i denne video", + "search_filters_date_option_none": "Enhver dato", + "search_filters_type_option_all": "Enhver type", + "search_filters_duration_option_none": "Enhver varighed", + "search_filters_duration_option_medium": "Medium (4 - 20 minutter)", + "search_filters_features_option_vr180": "VR180", + "generic_channels_count": "{{count}} kanal", + "generic_channels_count_plural": "{{count}} kanaler", + "Import YouTube playlist (.csv)": "Importer YouTube playliste (.csv)", + "Standard YouTube license": "Standard Youtube-licens", + "Album: ": "Album: ", + "Channel Sponsor": "Kanal-sponsor", + "Song: ": "Sang: ", + "channel_tab_playlists_label": "Playlister", + "channel_tab_channels_label": "Kanaler", + "Artist: ": "Kunstner: ", + "search_filters_date_label": "Uploaddato", + "generic_button_delete": "Slet", + "generic_button_edit": "Rediger", + "generic_button_save": "Gem", + "generic_button_cancel": "Afbryd", + "generic_button_rss": "RSS", + "Popular enabled: ": "Populær aktiveret: ", + "search_filters_apply_button": "Anvend udvalgte filtre", + "channel_tab_shorts_label": "Shorts", + "channel_tab_streams_label": "Livestreams", + "channel_tab_podcasts_label": "Podcasts", + "channel_tab_releases_label": "Udgivelser", + "Download is disabled": "Download er slået fra", + "error_video_not_in_playlist": "Den ønskede video findes ikke i denne playliste. <a href=\"`x`\">Klik her for playlistens startside.</a>" } diff --git a/locales/de.json b/locales/de.json index 59c6a49c..756aff76 100644 --- a/locales/de.json +++ b/locales/de.json @@ -148,7 +148,7 @@ "Whitelisted regions: ": "Erlaubte Regionen: ", "Blacklisted regions: ": "Unerlaubte Regionen: ", "Shared `x`": "Geteilt `x`", - "Premieres in `x`": "Zuerst gesehen in `x`", + "Premieres in `x`": "Premiere in `x`", "Premieres `x`": "Erster Start `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hallo! Anscheinend haben Sie JavaScript deaktiviert. Klicken Sie hier um Kommentare anzuzeigen, beachten sie dass es etwas länger dauern kann um sie zu laden.", "View YouTube comments": "YouTube Kommentare anzeigen", @@ -486,5 +486,6 @@ "channel_tab_podcasts_label": "Podcasts", "channel_tab_releases_label": "Veröffentlichungen", "generic_channels_count": "{{count}} Kanal", - "generic_channels_count_plural": "{{count}} Kanäle" + "generic_channels_count_plural": "{{count}} Kanäle", + "Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)" } diff --git a/locales/en-US.json b/locales/en-US.json index a9f78165..da83767c 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -1,4 +1,9 @@ { + "Add to playlist": "Add to playlist", + "Add to playlist: ": "Add to playlist: ", + "Answer": "Answer", + "Search for videos": "Search for videos", + "The Popular feed has been disabled by the administrator.": "The Popular feed has been disabled by the administrator.", "generic_channels_count": "{{count}} channel", "generic_channels_count_plural": "{{count}} channels", "generic_views_count": "{{count}} view", @@ -487,5 +492,6 @@ "channel_tab_releases_label": "Releases", "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", - "channel_tab_channels_label": "Channels" + "channel_tab_channels_label": "Channels", + "toggle_theme": "Toggle Theme" } diff --git a/locales/es.json b/locales/es.json index 0b8463ea..7a41710e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -90,7 +90,7 @@ "preferences_notifications_only_label": "Mostrar solo notificaciones (si hay alguna): ", "Enable web notifications": "Habilitar notificaciones web", "`x` uploaded a video": "`x` subió un video", - "`x` is live": "`x` esta en vivo", + "`x` is live": "`x` está en directo", "preferences_category_data": "Preferencias de los datos", "Clear watch history": "Borrar el historial de reproducción", "Import/export data": "Importar/Exportar datos", @@ -102,7 +102,7 @@ "preferences_category_admin": "Preferencias de administrador", "preferences_default_home_label": "Página de inicio por defecto: ", "preferences_feed_menu_label": "Menú de fuentes: ", - "preferences_show_nick_label": "Mostrar nombre de usuario arriba: ", + "preferences_show_nick_label": "Mostrar nombre de usuario encima: ", "Top enabled: ": "¿Habilitar los destacados? ", "CAPTCHA enabled: ": "¿Habilitar los CAPTCHA? ", "Login enabled: ": "¿Habilitar el inicio de sesión? ", @@ -144,13 +144,13 @@ "License: ": "Licencia: ", "Family friendly? ": "¿Filtrar contenidos? ", "Wilson score: ": "Puntuación Wilson: ", - "Engagement: ": "Compromiso: ", + "Engagement: ": "Retención: ", "Whitelisted regions: ": "Regiones permitidas: ", "Blacklisted regions: ": "Regiones bloqueadas: ", "Shared `x`": "Compartido `x`", "Premieres in `x`": "Se estrena en `x`", "Premieres `x`": "Estrenos `x`", - "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, pero tengas en cuenta que pueden tardar un poco más en cargarse.", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, ten en cuenta que pueden tardar un poco más en cargar.", "View YouTube comments": "Ver los comentarios de YouTube", "View more comments on Reddit": "Ver más comentarios en Reddit", "View `x` comments": { @@ -312,7 +312,7 @@ "Download as: ": "Descargar como: ", "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(editado)", - "YouTube comment permalink": "Enlace permanente de YouTube del comentario", + "YouTube comment permalink": "Enlace permanente de comentario de YouTube", "permalink": "enlace permanente", "`x` marked it with a ❤": "`x` lo ha marcado con un ❤", "Audio mode": "Modo de audio", @@ -324,10 +324,10 @@ "search_filters_sort_option_rating": "Valoración", "search_filters_sort_option_date": "Fecha de subida", "search_filters_sort_option_views": "Visualizaciones", - "search_filters_type_label": "tipo de contenido", - "search_filters_duration_label": "duración", - "search_filters_features_label": "funcionalidades", - "search_filters_sort_label": "ordenar", + "search_filters_type_label": "Tipo de contenido", + "search_filters_duration_label": "Duración", + "search_filters_features_label": "Funcionalidades", + "search_filters_sort_label": "Ordenar", "search_filters_date_option_hour": "Última hora", "search_filters_date_option_today": "Hoy", "search_filters_date_option_week": "Esta semana", @@ -390,43 +390,58 @@ "search_filters_features_option_three_sixty": "360°", "videoinfo_watch_on_youTube": "Ver en YouTube", "preferences_save_player_pos_label": "Guardar posición de reproducción: ", - "generic_views_count": "{{count}} visualización", - "generic_views_count_plural": "{{count}} visualizaciones", - "generic_subscribers_count": "{{count}} suscriptor", - "generic_subscribers_count_plural": "{{count}} suscriptores", - "generic_subscriptions_count": "{{count}} suscripción", - "generic_subscriptions_count_plural": "{{count}} suscripciones", - "subscriptions_unseen_notifs_count": "{{count}} notificación no vista", - "subscriptions_unseen_notifs_count_plural": "{{count}} notificaciones no vistas", - "generic_count_days": "{{count}} día", - "generic_count_days_plural": "{{count}} días", - "comments_view_x_replies": "Ver {{count}} respuesta", - "comments_view_x_replies_plural": "Ver {{count}} respuestas", - "generic_count_weeks": "{{count}} semana", - "generic_count_weeks_plural": "{{count}} semanas", - "generic_playlists_count": "{{count}} lista de reproducción", - "generic_playlists_count_plural": "{{count}} listas de reproducciones", - "generic_videos_count": "{{count}} video", - "generic_videos_count_plural": "{{count}} video", - "generic_count_months": "{{count}} mes", - "generic_count_months_plural": "{{count}} meses", - "comments_points_count": "{{count}} punto", - "comments_points_count_plural": "{{count}} puntos", - "generic_count_years": "{{count}} año", - "generic_count_years_plural": "{{count}} años", - "generic_count_hours": "{{count}} hora", - "generic_count_hours_plural": "{{count}} horas", - "generic_count_minutes": "{{count}} minuto", - "generic_count_minutes_plural": "{{count}} minutos", - "generic_count_seconds": "{{count}} segundo", - "generic_count_seconds_plural": "{{count}} segundos", + "generic_views_count_0": "{{count}} visualización", + "generic_views_count_1": "{{count}} visualizaciones", + "generic_views_count_2": "{{count}} visualizaciones", + "generic_subscribers_count_0": "{{count}} suscriptor", + "generic_subscribers_count_1": "{{count}} suscriptores", + "generic_subscribers_count_2": "{{count}} suscriptores", + "generic_subscriptions_count_0": "{{count}} suscripción", + "generic_subscriptions_count_1": "{{count}} suscripciones", + "generic_subscriptions_count_2": "{{count}} suscripciones", + "subscriptions_unseen_notifs_count_0": "{{count}} notificación sin ver", + "subscriptions_unseen_notifs_count_1": "{{count}} notificaciones sin ver", + "subscriptions_unseen_notifs_count_2": "{{count}} notificaciones sin ver", + "generic_count_days_0": "{{count}} día", + "generic_count_days_1": "{{count}} días", + "generic_count_days_2": "{{count}} días", + "comments_view_x_replies_0": "Ver {{count}} respuesta", + "comments_view_x_replies_1": "Ver {{count}} respuestas", + "comments_view_x_replies_2": "Ver {{count}} respuestas", + "generic_count_weeks_0": "{{count}} semana", + "generic_count_weeks_1": "{{count}} semanas", + "generic_count_weeks_2": "{{count}} semanas", + "generic_playlists_count_0": "{{count}} lista de reproducción", + "generic_playlists_count_1": "{{count}} listas de reproducciones", + "generic_playlists_count_2": "{{count}} listas de reproducciones", + "generic_videos_count_0": "{{count}} video", + "generic_videos_count_1": "{{count}} videos", + "generic_videos_count_2": "{{count}} videos", + "generic_count_months_0": "{{count}} mes", + "generic_count_months_1": "{{count}} meses", + "generic_count_months_2": "{{count}} meses", + "comments_points_count_0": "{{count}} punto", + "comments_points_count_1": "{{count}} puntos", + "comments_points_count_2": "{{count}} puntos", + "generic_count_years_0": "{{count}} año", + "generic_count_years_1": "{{count}} años", + "generic_count_years_2": "{{count}} años", + "generic_count_hours_0": "{{count}} hora", + "generic_count_hours_1": "{{count}} horas", + "generic_count_hours_2": "{{count}} horas", + "generic_count_minutes_0": "{{count}} minuto", + "generic_count_minutes_1": "{{count}} minutos", + "generic_count_minutes_2": "{{count}} minutos", + "generic_count_seconds_0": "{{count}} segundo", + "generic_count_seconds_1": "{{count}} segundos", + "generic_count_seconds_2": "{{count}} segundos", "crash_page_before_reporting": "Antes de notificar un error asegúrate de que has:", "crash_page_switch_instance": "probado a <a href=\"`x`\">usar otra instancia</a>", "crash_page_read_the_faq": "leído las <a href=\"`x`\">Preguntas Frecuentes</a>", "crash_page_search_issue": "buscado <a href=\"`x`\">problemas existentes en GitHub</a>", "crash_page_you_found_a_bug": "¡Parece que has encontrado un error en Invidious!", "crash_page_refresh": "probado a <a href=\"`x`\">recargar la página</a>", - "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, <a href=\"`x`\">abre una nueva incidencia en GitHub</a> (preferiblemente en inglés) e incluye verbatim el siguiente texto en tu mensaje:", + "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, <a href=\"`x`\">abre una nueva incidencia en GitHub</a> (preferiblemente en inglés) e incluye el siguiente texto en tu mensaje (NO traduzcas este texto):", "English (United States)": "Inglés (Estados Unidos)", "Cantonese (Hong Kong)": "Cantonés (Hong Kong)", "Dutch (auto-generated)": "Neerlandés (generados automáticamente)", @@ -454,14 +469,15 @@ "search_message_no_results": "No se han encontrado resultados.", "search_message_change_filters_or_query": "Pruebe ampliar la consulta de búsqueda y/o a cambiar los filtros.", "search_filters_title": "Filtros", - "search_filters_date_label": "fecha de subida", + "search_filters_date_label": "Fecha de subida", "search_filters_date_option_none": "Cualquier fecha", "search_filters_type_option_all": "Cualquier tipo", "search_filters_duration_option_none": "Cualquier duración", "search_filters_features_option_vr180": "VR180", "search_filters_apply_button": "Aplicar filtros", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", + "tokens_count_0": "{{count}} token", + "tokens_count_1": "{{count}} tokens", + "tokens_count_2": "{{count}} tokens", "search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.", "Popular enabled: ": "¿Habilitar la sección popular? ", "error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>", @@ -485,6 +501,9 @@ "generic_button_rss": "RSS", "channel_tab_podcasts_label": "Podcasts", "channel_tab_releases_label": "Publicaciones", - "generic_channels_count": "{{count}} canal", - "generic_channels_count_plural": "{{count}} canales" + "generic_channels_count_0": "{{count}} canal", + "generic_channels_count_1": "{{count}} canales", + "generic_channels_count_2": "{{count}} canales", + "Import YouTube watch history (.json)": "Importar el historial de las visualizaciones de YouTube (.json)", + "toggle_theme": "Alternar tema" } diff --git a/locales/fa.json b/locales/fa.json index 9b6c625d..d0251201 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -1,9 +1,14 @@ { - "generic_views_count_0": "{{count}} بازدید", - "generic_videos_count_0": "{{count}} ویدئو", - "generic_playlists_count_0": "{{count}} فهرست پخش", - "generic_subscribers_count_0": "{{count}} دنبال کننده", - "generic_subscriptions_count_0": "{{count}} اشتراک ها", + "generic_views_count": "{{count}} بازدید", + "generic_views_count_plural": "{{count}} بازدید", + "generic_videos_count": "{{count}} ویدئو", + "generic_videos_count_plural": "{{count}} ویدئو", + "generic_playlists_count": "{{count}} فهرست پخش", + "generic_playlists_count_plural": "{{count}} فهرست پخش", + "generic_subscribers_count": "{{count}} دنبال کننده", + "generic_subscribers_count_plural": "{{count}} دنبال کننده", + "generic_subscriptions_count": "{{count}} اشتراک", + "generic_subscriptions_count_plural": "{{count}} اشتراک", "LIVE": "زنده", "Shared `x` ago": "`x` پیش به اشتراک گذاشته شده", "Unsubscribe": "لغو اشتراک", @@ -117,13 +122,15 @@ "Subscription manager": "مدیریت اشتراک", "Token manager": "مدیر توکن", "Token": "توکن", - "tokens_count_0": "{{count}} توکن ها", + "tokens_count": "{{count}} توکن", + "tokens_count_plural": "{{count}} توکن", "Import/export": "وارد کردن/خارج کردن", "unsubscribe": "لغو اشتراک", "revoke": "ابطال", "Subscriptions": "اشتراک ها", - "subscriptions_unseen_notifs_count_0": "{{count}} اعلان نادیده", - "search": "جستجو", + "subscriptions_unseen_notifs_count": "{{count}} اعلان نادیده", + "subscriptions_unseen_notifs_count_plural": "{{count}} اعلان نادیده", + "search": "جست و جو", "Log out": "خروج", "Released under the AGPLv3 on Github.": "منتشر شده تحت پروانه AGPLv3 روی گیتهاب.", "Source available here.": "منبع اینجا دردسترس است.", @@ -183,10 +190,12 @@ "This channel does not exist.": "این کانال وجود ندارد.", "Could not get channel info.": "نمیتوان اطلاعات کانال را دریافت کرد.", "Could not fetch comments": "نمیتوان نظرات را دریافت کرد", - "comments_view_x_replies_0": "نمایش {{count}} پاسخ ها", + "comments_view_x_replies": "نمایش {{count}} پاسخ", + "comments_view_x_replies_plural": "نمایش {{count}} پاسخ", "`x` ago": "`x` پیش", "Load more": "بارگذاری بیشتر", - "comments_points_count_0": "{{count}} نقطه ها", + "comments_points_count": "{{count}} نقطه", + "comments_points_count_plural": "{{count}} نقطه", "Could not create mix.": "نمیتوان میکس ساخت.", "Empty playlist": "سیاههٔ پخش خالی", "Not a playlist.": "یک سیاههٔ پخش نیست.", @@ -304,16 +313,23 @@ "Yiddish": "ییدیش", "Yoruba": "یوروبایی", "Zulu": "زولو", - "generic_count_years_0": "{{count}} سال", - "generic_count_months_0": "{{count}} ماه", - "generic_count_weeks_0": "{{count}} هفته", - "generic_count_days_0": "{{count}} روز", - "generic_count_hours_0": "{{count}} ساعت", - "generic_count_minutes_0": "{{count}} دقیقه", - "generic_count_seconds_0": "{{count}} ثانیه", + "generic_count_years": "{{count}} سال", + "generic_count_years_plural": "{{count}} سال", + "generic_count_months": "{{count}} ماه", + "generic_count_months_plural": "{{count}} ماه", + "generic_count_weeks": "{{count}} هفته", + "generic_count_weeks_plural": "{{count}} هفته", + "generic_count_days": "{{count}} روز", + "generic_count_days_plural": "{{count}} روز", + "generic_count_hours": "{{count}} ساعت", + "generic_count_hours_plural": "{{count}} ساعت", + "generic_count_minutes": "{{count}} دقیقه", + "generic_count_minutes_plural": "{{count}} دقیقه", + "generic_count_seconds": "{{count}} ثانیه", + "generic_count_seconds_plural": "{{count}} ثانیه", "Fallback comments: ": "نظرات عقب گرد: ", "Popular": "محبوب", - "Search": "جستجو", + "Search": "جست و جو", "Top": "بالا", "About": "درباره", "Rating: ": "رتبه دهی: ", @@ -445,5 +461,28 @@ "Song: ": "آهنگ: ", "Channel Sponsor": "اسپانسر کانال", "Standard YouTube license": "پروانه استاندارد YouTube", - "search_message_use_another_instance": " شما همچنین میتوانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>." + "search_message_use_another_instance": " شما همچنین میتوانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>.", + "Download is disabled": "دریافت غیرفعال است", + "crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:", + "playlist_button_add_items": "افزودن ویدیو", + "user_saved_playlists": "فهرستهای پخش ذخیره شده", + "crash_page_refresh": "که صفحه را <a href=\"`x`\">بازنشانی</a> کردهاید", + "generic_button_save": "ذخیره", + "generic_button_cancel": "لغو", + "generic_channels_count": "{{count}} کانال", + "generic_channels_count_plural": "{{count}} کانال", + "generic_button_edit": "ویرایش", + "crash_page_switch_instance": "که تلاش کردهاید <a href=\"`x`\">از یک نمونهٔ دیگر</a> استفاده کنید", + "generic_button_rss": "خوراک RSS", + "crash_page_read_the_faq": "که <a href=\"`x`\">سوالات بیشتر پرسیده شده (FAQ)</a> را خواندهاید", + "generic_button_delete": "حذف", + "Import YouTube playlist (.csv)": "واردکردن فهرستپخش YouTube (.csv)", + "Import YouTube watch history (.json)": "وارد کردن فهرست پخش YouTube (.json)", + "crash_page_you_found_a_bug": "به نظر میرسد که ایرادی در Invidious پیدا کردهاید!", + "channel_tab_podcasts_label": "پادکستها", + "channel_tab_streams_label": "پخش زندهها", + "channel_tab_shorts_label": "Shortها", + "channel_tab_playlists_label": "فهرستهای پخش", + "channel_tab_channels_label": "کانالها", + "error_video_not_in_playlist": "ویدیوی درخواستی معلق به این فهرست پخش نیست. <a href=\"`x`\">کلیک کنید تا به صفحهٔ اصلی فهرست پخش بروید.</a>" } diff --git a/locales/fi.json b/locales/fi.json index 5d8578a5..14c2b0fc 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -14,7 +14,7 @@ "Clear watch history?": "Tyhjennä katseluhistoria?", "New password": "Uusi salasana", "New passwords must match": "Uusien salasanojen täytyy täsmätä", - "Authorize token?": "Valuutetaanko tunnus?", + "Authorize token?": "Valtuutetaanko tunnus?", "Authorize token for `x`?": "Valtuutetaanko tunnus `x`:lle?", "Yes": "Kyllä", "No": "Ei", diff --git a/locales/fr.json b/locales/fr.json index 772c81c8..251e88bc 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -503,5 +503,6 @@ "Download is disabled": "Le téléchargement est désactivé", "Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)", "channel_tab_releases_label": "Parutions", - "channel_tab_podcasts_label": "Émissions audio" + "channel_tab_podcasts_label": "Émissions audio", + "Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)" } diff --git a/locales/hi.json b/locales/hi.json index 21807c50..a7e0639a 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -476,7 +476,7 @@ "generic_button_cancel": "रद्द करें", "generic_button_rss": "आरएसएस", "generic_button_edit": "संपादित करें", - "generic_button_delete": "मिटाएं", + "generic_button_delete": "हटाएं", "playlist_button_add_items": "वीडियो जोड़ें", "Song: ": "गाना: ", "channel_tab_podcasts_label": "पाॅडकास्ट", @@ -484,5 +484,8 @@ "Import YouTube playlist (.csv)": "YouTube प्लेलिस्ट (.csv) आयात करें", "Standard YouTube license": "मानक यूट्यूब लाइसेंस", "Channel Sponsor": "चैनल प्रायोजक", - "Download is disabled": "डाउनलोड करना अक्षम है" + "Download is disabled": "डाउनलोड करना अक्षम है", + "generic_channels_count": "{{count}} चैनल", + "generic_channels_count_plural": "{{count}} चैनल", + "Import YouTube watch history (.json)": "YouTube पर देखने का इतिहास आयात करें (.json)" } diff --git a/locales/hr.json b/locales/hr.json index ef931202..2d86144f 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -503,5 +503,6 @@ "channel_tab_releases_label": "Izdanja", "generic_channels_count_0": "{{count}} kanal", "generic_channels_count_1": "{{count}} kanala", - "generic_channels_count_2": "{{count}} kanala" + "generic_channels_count_2": "{{count}} kanala", + "Import YouTube watch history (.json)": "Uvezi YouTube povijest gledanja (.json)" } diff --git a/locales/ia.json b/locales/ia.json new file mode 100644 index 00000000..19b6b0c0 --- /dev/null +++ b/locales/ia.json @@ -0,0 +1,41 @@ +{ + "New password": "Nove contrasigno", + "preferences_player_style_label": "Stylo de reproductor: ", + "preferences_region_label": "Pais de contento: ", + "oldest": "plus ancian", + "published": "data de publication", + "invidious": "Invidious", + "Image CAPTCHA": "Imagine CAPTCHA", + "newest": "plus nove", + "generic_button_save": "Salvar", + "Dark mode: ": "Modo obscur: ", + "preferences_dark_mode_label": "Thema: ", + "preferences_category_subscription": "Preferentias de subscription", + "last": "ultime", + "generic_button_cancel": "Cancellar", + "popular": "popular", + "Time (h:mm:ss):": "Tempore (h:mm:ss):", + "preferences_autoplay_label": "Reproduction automatic: ", + "Sign In": "Aperir le session", + "Log in": "Initiar le session", + "preferences_speed_label": "Velocitate per predefinition: ", + "preferences_comments_label": "Commentos predefinite: ", + "light": "clar", + "No": "Non", + "youtube": "YouTube", + "LIVE": "IN DIRECTE", + "reddit": "Reddit", + "preferences_category_player": "Preferentias de reproductor", + "Preferences": "Preferentias", + "preferences_quality_dash_option_auto": "Automatic", + "dark": "obscur", + "generic_button_rss": "RSS", + "Export": "Exportar", + "History": "Chronologia", + "Password": "Contrasigno", + "User ID": "ID de usator", + "E-mail": "E-mail", + "Delete account?": "Deler conto?", + "preferences_volume_label": "Volumine del reproductor: ", + "preferences_sort_label": "Ordinar le videos per: " +} diff --git a/locales/id.json b/locales/id.json index 8961880b..4c6e8548 100644 --- a/locales/id.json +++ b/locales/id.json @@ -469,5 +469,6 @@ "error_video_not_in_playlist": "Video yang diminta tidak ada dalam daftar putar ini. <a href=\"`x`\">Klik di sini untuk halaman beranda daftar putar.</a>", "generic_button_delete": "Hapus", "Import YouTube playlist (.csv)": "Impor daftar putar YouTube (.csv)", - "Standard YouTube license": "Lisensi YouTube standar" + "Standard YouTube license": "Lisensi YouTube standar", + "Import YouTube watch history (.json)": "Impor riwayat tontonan YouTube (.json)" } diff --git a/locales/it.json b/locales/it.json index 7e1b12c6..7b6bb5d9 100644 --- a/locales/it.json +++ b/locales/it.json @@ -503,5 +503,6 @@ "channel_tab_podcasts_label": "Podcast", "generic_channels_count_0": "{{count}} canale", "generic_channels_count_1": "{{count}} canali", - "generic_channels_count_2": "{{count}} canali" + "generic_channels_count_2": "{{count}} canali", + "Import YouTube watch history (.json)": "Importa la cronologia delle visualizzazioni di YouTube (.json)" } diff --git a/locales/ja.json b/locales/ja.json index 17e60998..2e3437bc 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -53,7 +53,7 @@ "preferences_category_player": "プレイヤーの設定", "preferences_video_loop_label": "常にループ: ", "preferences_autoplay_label": "自動再生: ", - "preferences_continue_label": "次の動画を自動再生: ", + "preferences_continue_label": "次の動画に移動: ", "preferences_continue_autoplay_label": "次の動画を自動再生: ", "preferences_listen_label": "音声モードを使用: ", "preferences_local_label": "動画視聴にプロキシを経由: ", @@ -68,7 +68,7 @@ "preferences_related_videos_label": "関連動画を表示: ", "preferences_annotations_label": "最初からアノテーションを表示: ", "preferences_extend_desc_label": "動画の説明文を自動的に拡張: ", - "preferences_vr_mode_label": "対話的な360°動画 (WebGL が必要): ", + "preferences_vr_mode_label": "対話的な360°動画 (WebGLが必要): ", "preferences_category_visual": "外観設定", "preferences_player_style_label": "プレイヤーのスタイル: ", "Dark mode: ": "ダークモード: ", @@ -125,9 +125,9 @@ "subscriptions_unseen_notifs_count_0": "{{count}}件の未読通知", "search": "検索", "Log out": "ログアウト", - "Released under the AGPLv3 on Github.": "GitHub 上で AGPLv3 の元で公開", + "Released under the AGPLv3 on Github.": "GitHub上でAGPLv3の元で公開", "Source available here.": "ソースはここで閲覧可能です。", - "View JavaScript license information.": "JavaScript ライセンス情報", + "View JavaScript license information.": "JavaScriptライセンス情報", "View privacy policy.": "個人情報保護方針", "Trending": "急上昇", "Public": "公開", @@ -144,7 +144,7 @@ "Show more": "もっと見る", "Show less": "表示を少なく", "Watch on YouTube": "YouTubeで視聴", - "Switch Invidious Instance": "Invidious インスタンスの変更", + "Switch Invidious Instance": "Invidiousインスタンスの変更", "Hide annotations": "アノテーションを隠す", "Show annotations": "アノテーションを表示", "Genre: ": "ジャンル: ", @@ -363,9 +363,9 @@ "search_filters_features_option_location": "場所", "search_filters_features_option_hdr": "HDR", "Current version: ": "現在のバージョン: ", - "next_steps_error_message": "下記のものを試して下さい: ", - "next_steps_error_message_refresh": "再読込", - "next_steps_error_message_go_to_youtube": "YouTubeへ", + "next_steps_error_message": "以下をお試してください: ", + "next_steps_error_message_refresh": "再読み込み", + "next_steps_error_message_go_to_youtube": "YouTubeを開く", "search_filters_duration_option_short": "4分未満", "footer_documentation": "説明書", "footer_source_code": "ソースコード", @@ -459,7 +459,7 @@ "Song: ": "曲: ", "Channel Sponsor": "チャンネルのスポンサー", "Standard YouTube license": "標準 Youtube ライセンス", - "Download is disabled": "ダウンロード: このインスタンスでは未対応", + "Download is disabled": "ダウンロード: このインスタンスは未対応", "Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)", "generic_button_delete": "削除", "generic_button_cancel": "キャンセル", @@ -469,5 +469,6 @@ "generic_button_save": "保存", "generic_button_rss": "RSS", "playlist_button_add_items": "動画を追加", - "generic_channels_count_0": "{{count}}個のチャンネル" + "generic_channels_count_0": "{{count}}個のチャンネル", + "Import YouTube watch history (.json)": "YouTube 視聴履歴をインポート (.json)" } diff --git a/locales/ko.json b/locales/ko.json index e496bd2a..c0257ee5 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -46,7 +46,7 @@ "source": "출처", "JavaScript license information": "자바스크립트 라이선스 정보", "An alternative front-end to YouTube": "유튜브의 프론트엔드 대안", - "History": "역사", + "History": "시청 기록", "Delete account?": "계정을 삭제 하시겠습니까?", "Export data as JSON": "JSON으로 데이터 내보내기", "Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)", @@ -351,7 +351,7 @@ "News": "뉴스", "Gaming": "게임", "Music": "음악", - "Default": "디폴트", + "Default": "전체", "Rating: ": "평점: ", "About": "정보", "Top": "최고", @@ -469,5 +469,6 @@ "generic_button_cancel": "취소", "generic_button_rss": "RSS", "channel_tab_releases_label": "출시", - "generic_channels_count_0": "{{count}} 채널" + "generic_channels_count_0": "{{count}} 채널", + "Import YouTube watch history (.json)": "유튜브 시청 기록 가져오기 (.json)" } diff --git a/locales/nb-NO.json b/locales/nb-NO.json index 08b1e0e2..cf0ee286 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -486,5 +486,6 @@ "generic_button_rss": "RSS", "playlist_button_add_items": "Legg til videoer", "generic_channels_count": "{{count}} kanal", - "generic_channels_count_plural": "{{count}} kanaler" + "generic_channels_count_plural": "{{count}} kanaler", + "Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)" } diff --git a/locales/nl.json b/locales/nl.json index aa5da731..a30bc5b5 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -107,10 +107,10 @@ "Report statistics: ": "Statistieken bijhouden? ", "Save preferences": "Instellingen opslaan", "Subscription manager": "Abonnementen beheren", - "Token manager": "Toegangssleutels beheren", + "Token manager": "Toegangssleutelbeheerder", "Token": "Toegangssleutel", "Import/export": "Importeren/Exporteren", - "unsubscribe": "Deabonneren", + "unsubscribe": "deabonneren", "revoke": "Intrekken", "Subscriptions": "Abonnementen", "search": "zoeken", @@ -357,7 +357,7 @@ "footer_original_source_code": "Originele bron-code", "footer_modfied_source_code": "Gewijzigde bron-code", "adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats", - "next_steps_error_message": "Waarna u moet proberen om: ", + "next_steps_error_message": "Daarna moet u proberen om: ", "footer_source_code": "Bron-code", "search_filters_duration_option_long": "Lang (> 20 minuten)", "preferences_quality_option_dash": "DASH (adaptieve kwaliteit)", @@ -462,5 +462,30 @@ "Spanish (auto-generated)": "Spaans (automatisch gegenereerd)", "crash_page_you_found_a_bug": "Je lijkt een bug in Invidious tegengekomen te zijn!", "search_filters_duration_option_medium": "Gemiddeld (4 - 20 minuten)", - "crash_page_report_issue": "Indien het bovenstaande niet hielp, gelieve dan <a href=\"`x`\">een nieuw ticket op GitHub</a> te openen (liefst in het Engels) en neem de volgende tekst op in je bericht (gelieve deze NIET te vertalen):" + "crash_page_report_issue": "Indien het bovenstaande niet hielp, gelieve dan <a href=\"`x`\">een nieuw ticket op GitHub</a> te openen (liefst in het Engels) en neem de volgende tekst op in je bericht (gelieve deze NIET te vertalen):", + "channel_tab_podcasts_label": "Podcasts", + "Download is disabled": "Downloaden is uitgeschakeld", + "Channel Sponsor": "Kanaalsponsor", + "channel_tab_streams_label": "Livestreams", + "playlist_button_add_items": "Video's toevoegen", + "Artist: ": "Artiest: ", + "generic_button_save": "Opslaan", + "generic_button_cancel": "Annuleren", + "Album: ": "Album: ", + "channel_tab_shorts_label": "Shorts", + "channel_tab_releases_label": "Uitgaves", + "Song: ": "Lied: ", + "generic_channels_count": "{{count}} kanaal", + "generic_channels_count_plural": "{{count}} kanalen", + "Popular enabled: ": "Populair geactiveerd: ", + "channel_tab_playlists_label": "Afspeellijsten", + "generic_button_edit": "Bewerken", + "Music in this video": "Muziek in deze video", + "generic_button_rss": "RSS", + "channel_tab_channels_label": "Kanalen", + "error_video_not_in_playlist": "De gevraagde video bestaat niet in deze afspeellijst. <a href=\"`x`\">Klik hier voor de startpagina van de afspeellijst.</a>", + "generic_button_delete": "Verwijderen", + "Import YouTube playlist (.csv)": "YouTube-afspeellijst importeren (.csv)", + "Standard YouTube license": "Standaard YouTube-licentie", + "Import YouTube watch history (.json)": "YouTube-kijkgeschiedenis importeren (.json)" } diff --git a/locales/pl.json b/locales/pl.json index 313f11cb..0d18e90a 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -492,7 +492,7 @@ "Song: ": "Piosenka: ", "Channel Sponsor": "Sponsor kanału", "Standard YouTube license": "Standardowa licencja YouTube", - "Import YouTube playlist (.csv)": "Importuj playlistę YouTube (.csv)", + "Import YouTube playlist (.csv)": "Importuj playlistę z YouTube (.csv)", "generic_button_edit": "Edytuj", "generic_button_cancel": "Anuluj", "generic_button_rss": "RSS", @@ -503,5 +503,7 @@ "playlist_button_add_items": "Dodaj filmy", "generic_channels_count_0": "{{count}} kanał", "generic_channels_count_1": "{{count}} kanały", - "generic_channels_count_2": "{{count}} kanałów" + "generic_channels_count_2": "{{count}} kanałów", + "Import YouTube watch history (.json)": "Importuj historię oglądania z YouTube (.json)", + "toggle_theme": "Przełącz motyw" } diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 1e089723..af14eb29 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -503,5 +503,7 @@ "generic_button_rss": "RSS", "generic_channels_count_0": "{{count}} canal", "generic_channels_count_1": "{{count}} canais", - "generic_channels_count_2": "{{count}} canais" + "generic_channels_count_2": "{{count}} canais", + "Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)", + "toggle_theme": "Alternar Tema" } diff --git a/locales/pt.json b/locales/pt.json index e7cc4810..c1d8b5b4 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -1,7 +1,7 @@ { - "search_filters_type_option_show": "Espetáculo", + "search_filters_type_option_show": "Série", "search_filters_sort_option_views": "Visualizações", - "search_filters_sort_option_date": "Data de envio", + "search_filters_sort_option_date": "Data de carregamento", "search_filters_sort_option_rating": "Avaliação", "search_filters_sort_option_relevance": "Relevância", "Switch Invidious Instance": "Mudar a instância do Invidious", @@ -13,7 +13,7 @@ "preferences_category_misc": "Preferências diversas", "preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ", "preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ", - "next_steps_error_message_go_to_youtube": "Ir ao YouTube", + "next_steps_error_message_go_to_youtube": "Ir para o YouTube", "next_steps_error_message": "Pode tentar as seguintes opções: ", "next_steps_error_message_refresh": "Atualizar", "search_filters_features_option_hdr": "HDR", @@ -44,20 +44,27 @@ "Default": "Predefinido", "Top": "Destaques", "Search": "Pesquisar", - "generic_count_years": "{{count}} segundo", - "generic_count_years_plural": "{{count}} segundos", - "generic_count_months": "{{count}} minuto", - "generic_count_months_plural": "{{count}} minutos", - "generic_count_weeks": "{{count}} hora", - "generic_count_weeks_plural": "{{count}} horas", - "generic_count_days": "{{count}} dia", - "generic_count_days_plural": "{{count}} dias", - "generic_count_hours": "{{count}} seman", - "generic_count_hours_plural": "{{count}} semanas", - "generic_count_minutes": "{{count}} mês", - "generic_count_minutes_plural": "{{count}} meses", - "generic_count_seconds": "{{count}} ano", - "generic_count_seconds_plural": "{{count}} anos", + "generic_count_years_0": "{{count}} ano", + "generic_count_years_1": "{{count}} anos", + "generic_count_years_2": "{{count}} anos", + "generic_count_months_0": "{{count}} mês", + "generic_count_months_1": "{{count}} meses", + "generic_count_months_2": "{{count}} meses", + "generic_count_weeks_0": "{{count}} semana", + "generic_count_weeks_1": "{{count}} semanas", + "generic_count_weeks_2": "{{count}} semanas", + "generic_count_days_0": "{{count}} dia", + "generic_count_days_1": "{{count}} dias", + "generic_count_days_2": "{{count}} dias", + "generic_count_hours_0": "{{count}} hora", + "generic_count_hours_1": "{{count}} horas", + "generic_count_hours_2": "{{count}} horas", + "generic_count_minutes_0": "{{count}} minuto", + "generic_count_minutes_1": "{{count}} minutos", + "generic_count_minutes_2": "{{count}} minutos", + "generic_count_seconds_0": "{{count}} segundo", + "generic_count_seconds_1": "{{count}} segundos", + "generic_count_seconds_2": "{{count}} segundos", "Chinese (Traditional)": "Chinês (tradicional)", "Chinese (Simplified)": "Chinês (simplificado)", "Could not pull trending pages.": "Não foi possível obter as páginas de tendências.", @@ -75,7 +82,7 @@ "Import/export data": "Importar / exportar dados", "preferences_annotations_label": "Mostrar anotações sempre: ", "preferences_continue_label": "Reproduzir sempre o próximo: ", - "Sign In": "Iniciar sessão", + "Sign In": "Entrar", "Log in/register": "Iniciar sessão/registar", "Delete account?": "Eliminar conta?", "Import and Export Data": "Importar e exportar dados", @@ -167,8 +174,9 @@ "Log out": "Terminar sessão", "Subscriptions": "Subscrições", "revoke": "revogar", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", + "tokens_count_0": "{{count}} Token", + "tokens_count_1": "{{count}} Tokens", + "tokens_count_2": "{{count}} Tokens", "Token": "Token", "Token manager": "Gerir tokens", "Subscription manager": "Gerir subscrições", @@ -402,31 +410,39 @@ "videoinfo_youTube_embed_link": "Incorporar", "preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ", "download_subtitles": "Legendas - `x` (.vtt)", - "generic_views_count": "{{count}} visualização", - "generic_views_count_plural": "{{count}} visualizações", + "generic_views_count_0": "{{count}} visualização", + "generic_views_count_1": "{{count}} visualizações", + "generic_views_count_2": "{{count}} visualizações", "videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`", "user_saved_playlists": "`x` listas de reprodução guardadas", - "generic_videos_count": "{{count}} vídeo", - "generic_videos_count_plural": "{{count}} vídeos", - "generic_playlists_count": "{{count}} lista de reprodução", - "generic_playlists_count_plural": "{{count}} listas de reprodução", - "subscriptions_unseen_notifs_count": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", - "comments_view_x_replies": "Ver {{count}} resposta", - "comments_view_x_replies_plural": "Ver {{count}} respostas", - "generic_subscribers_count": "{{count}} inscrito", - "generic_subscribers_count_plural": "{{count}} inscritos", - "generic_subscriptions_count": "{{count}} inscrição", - "generic_subscriptions_count_plural": "{{count}} inscrições", - "comments_points_count": "{{count}} ponto", - "comments_points_count_plural": "{{count}} pontos", + "generic_videos_count_0": "{{count}} vídeo", + "generic_videos_count_1": "{{count}} vídeos", + "generic_videos_count_2": "{{count}} vídeos", + "generic_playlists_count_0": "{{count}} lista de reprodução", + "generic_playlists_count_1": "{{count}} listas de reprodução", + "generic_playlists_count_2": "{{count}} listas de reprodução", + "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", + "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", + "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", + "comments_view_x_replies_0": "Ver {{count}} resposta", + "comments_view_x_replies_1": "Ver {{count}} respostas", + "comments_view_x_replies_2": "Ver {{count}} respostas", + "generic_subscribers_count_0": "{{count}} inscrito", + "generic_subscribers_count_1": "{{count}} inscritos", + "generic_subscribers_count_2": "{{count}} inscritos", + "generic_subscriptions_count_0": "{{count}} inscrição", + "generic_subscriptions_count_1": "{{count}} inscrições", + "generic_subscriptions_count_2": "{{count}} inscrições", + "comments_points_count_0": "{{count}} ponto", + "comments_points_count_1": "{{count}} pontos", + "comments_points_count_2": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se:", "crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>", "crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>", "crash_page_read_the_faq": "leia as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>", "crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no GitHub</a>", - "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto tal qual (NÃO o traduza):", + "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto (NÃO o traduza):", "user_created_playlists": "`x` listas de reprodução criadas", "search_filters_title": "Filtro", "Chinese (Taiwan)": "Chinês (Taiwan)", @@ -464,7 +480,7 @@ "search_filters_type_option_all": "Qualquer tipo", "search_filters_duration_option_none": "Qualquer duração", "Popular enabled: ": "Página \"popular\" ativada: ", - "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para a página inicial da lista de reprodução.</a>", + "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para voltar à página inicial da lista de reprodução.</a>", "channel_tab_playlists_label": "Listas de reprodução", "channel_tab_channels_label": "Canais", "channel_tab_shorts_label": "Curtos", @@ -484,5 +500,10 @@ "channel_tab_releases_label": "Lançamentos", "generic_button_save": "Salvar", "generic_button_cancel": "Cancelar", - "playlist_button_add_items": "Adicionar vídeos" + "playlist_button_add_items": "Adicionar vídeos", + "generic_channels_count_0": "{{count}} canal", + "generic_channels_count_1": "{{count}} canais", + "generic_channels_count_2": "{{count}} canais", + "Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)", + "toggle_theme": "Trocar tema" } diff --git a/locales/ru.json b/locales/ru.json index 2769f3ab..61bf9e92 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -8,14 +8,14 @@ "newest": "сначала новые", "oldest": "сначала старые", "popular": "популярные", - "last": "недавние", + "last": "последние", "Next page": "Следующая страница", "Previous page": "Предыдущая страница", "Clear watch history?": "Очистить историю просмотров?", "New password": "Новый пароль", "New passwords must match": "Новые пароли не совпадают", "Authorize token?": "Авторизовать токен?", - "Authorize token for `x`?": "Авторизовать токен для `x`?", + "Authorize token for `x`?": "Токен авторизации для `x`?", "Yes": "Да", "No": "Нет", "Import and Export Data": "Импорт и экспорт данных", @@ -29,7 +29,7 @@ "Export subscriptions as OPML": "Экспортировать подписки в формате OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в формате OPML (для NewPipe и FreeTube)", "Export data as JSON": "Экспортировать данные Invidious в формате JSON", - "Delete account?": "Удалить учётку?", + "Delete account?": "Удалить учётную запись?", "History": "История", "An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube", "JavaScript license information": "Информация о лицензиях JavaScript", @@ -42,7 +42,7 @@ "Text CAPTCHA": "Текстовая капча (англ.)", "Image CAPTCHA": "Капча-картинка", "Sign In": "Войти", - "Register": "Зарегистрироваться", + "Register": "Регистрация", "E-mail": "Эл. почта", "Preferences": "Настройки", "preferences_category_player": "Настройки проигрывателя", @@ -61,7 +61,7 @@ "preferences_captions_label": "Основной язык субтитров: ", "Fallback captions: ": "Дополнительный язык субтитров: ", "preferences_related_videos_label": "Показывать похожие видео? ", - "preferences_annotations_label": "Всегда показывать аннотации? ", + "preferences_annotations_label": "Показывать аннотации по умолчанию: ", "preferences_extend_desc_label": "Автоматически раскрывать описание видео: ", "preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ", "preferences_category_visual": "Настройки сайта", @@ -77,13 +77,13 @@ "preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ", "Redirect homepage to feed: ": "Показывать подписки на главной странице: ", "preferences_max_results_label": "Число видео в ленте: ", - "preferences_sort_label": "Сортировать видео: ", - "published": "по дате публикации", - "published - reverse": "по дате публикации в обратном порядке", - "alphabetically": "по алфавиту", - "alphabetically - reverse": "по алфавиту в обратном порядке", - "channel name": "по названию канала", - "channel name - reverse": "по названию канала в обратном порядке", + "preferences_sort_label": "Сортировать видео по: ", + "published": "дате публикации", + "published - reverse": "дате публикации в обратном порядке", + "alphabetically": "алфавиту", + "alphabetically - reverse": "алфавиту в обратном порядке", + "channel name": "названию канала", + "channel name - reverse": "названию канала в обратном порядке", "Only show latest video from channel: ": "Показывать только последние видео с каналов: ", "Only show latest unwatched video from channel: ": "Показывать только последние непросмотренные видео с канала: ", "preferences_unseen_only_label": "Показывать только непросмотренные видео: ", @@ -134,8 +134,8 @@ "Title": "Заголовок", "Playlist privacy": "Видимость плейлиста", "Editing playlist `x`": "Редактирование плейлиста `x`", - "Show more": "Развернуть", - "Show less": "Свернуть", + "Show more": "Показать больше", + "Show less": "Показать меньше", "Watch on YouTube": "Смотреть на YouTube", "Switch Invidious Instance": "Сменить зеркало Invidious", "Hide annotations": "Скрыть аннотации", @@ -414,7 +414,7 @@ "generic_count_days_0": "{{count}} день", "generic_count_days_1": "{{count}} дня", "generic_count_days_2": "{{count}} дней", - "preferences_quality_dash_option_auto": "Автоматическое", + "preferences_quality_dash_option_auto": "Авто", "preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_720p": "720p", "generic_subscriptions_count_0": "{{count}} подписка", @@ -466,7 +466,7 @@ "search_filters_features_option_three_sixty": "360°", "Video unavailable": "Видео недоступно", "preferences_save_player_pos_label": "Запоминать позицию: ", - "preferences_region_label": "Страна: ", + "preferences_region_label": "Страна источник ", "preferences_watch_history_label": "Включить историю просмотров: ", "search_filters_title": "Фильтр", "search_filters_duration_option_none": "Любой длины", @@ -476,7 +476,7 @@ "search_message_no_results": "Ничего не найдено.", "search_message_use_another_instance": " Дополнительно вы можете <a href=\"`x`\">поискать на других зеркалах</a>.", "search_filters_features_option_vr180": "VR180", - "search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос или изменить фильтры.", + "search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос и/или изменить фильтры.", "search_filters_duration_option_medium": "Средние (4 - 20 минут)", "search_filters_apply_button": "Применить фильтры", "Popular enabled: ": "Популярное включено: ", @@ -503,5 +503,6 @@ "channel_tab_podcasts_label": "Подкасты", "generic_channels_count_0": "{{count}} канал", "generic_channels_count_1": "{{count}} канала", - "generic_channels_count_2": "{{count}} каналов" + "generic_channels_count_2": "{{count}} каналов", + "Import YouTube watch history (.json)": "Импортировать историю просмотра из YouTube (.json)" } diff --git a/locales/sl.json b/locales/sl.json index 9a912f2d..3803d09c 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -520,5 +520,6 @@ "generic_channels_count_0": "{{count}} kanal", "generic_channels_count_1": "{{count}} kanala", "generic_channels_count_2": "{{count}} kanali", - "generic_channels_count_3": "{{count}} kanalov" + "generic_channels_count_3": "{{count}} kanalov", + "Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)" } diff --git a/locales/sq.json b/locales/sq.json index 41d4161c..363a70b0 100644 --- a/locales/sq.json +++ b/locales/sq.json @@ -79,7 +79,7 @@ "invidious": "Invidious", "preferences_captions_label": "Titra parazgjedhje: ", "preferences_extend_desc_label": "Zgjero automatikisht përshkrimin e videos: ", - "preferences_player_style_label": "Silt lojtësi: ", + "preferences_player_style_label": "Stil lojtësi: ", "Dark mode: ": "Mënyra e errët: ", "preferences_dark_mode_label": "Temë: ", "dark": "e errët", @@ -477,5 +477,12 @@ "channel_tab_releases_label": "Hedhje në qarkullim", "Song: ": "Pjesë: ", "Import YouTube playlist (.csv)": "Importoni luajlistë YouTube (.csv)", - "Standard YouTube license": "Licencë YouTube standarde" + "Standard YouTube license": "Licencë YouTube standarde", + "published - reverse": "publikuar më - së prapthi", + "channel_tab_podcasts_label": "Podcast-e", + "channel name - reverse": "emër kanali - së prapthi", + "Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)", + "preferences_local_label": "Video përmes ndërmjetësi: ", + "Fallback captions: ": "Titra nga halli: ", + "Erroneous challenge": "Zgjidhje e gabuar" } diff --git a/locales/sr.json b/locales/sr.json index f0e5518d..b4a98da6 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -503,5 +503,6 @@ "crash_page_you_found_a_bug": "Izgleda da ste pronašli grešku u Invidious-u!", "generic_views_count_0": "{{count}} pregled", "generic_views_count_1": "{{count}} pregleda", - "generic_views_count_2": "{{count}} pregleda" + "generic_views_count_2": "{{count}} pregleda", + "Import YouTube watch history (.json)": "Uvezi YouTube istoriju gledanja (.json)" } diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index bf439b28..52ac4116 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -503,5 +503,7 @@ "crash_page_you_found_a_bug": "Изгледа да сте пронашли грешку у Invidious-у!", "generic_views_count_0": "{{count}} преглед", "generic_views_count_1": "{{count}} прегледа", - "generic_views_count_2": "{{count}} прегледа" + "generic_views_count_2": "{{count}} прегледа", + "Import YouTube watch history (.json)": "Увези YouTube историју гледањa (.json)", + "toggle_theme": "Укључи тему" } diff --git a/locales/sv-SE.json b/locales/sv-SE.json index a319fffd..db3486df 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -20,15 +20,15 @@ "No": "Nej", "Import and Export Data": "Importera och exportera data", "Import": "Importera", - "Import Invidious data": "Importera Invidious-data", - "Import YouTube subscriptions": "Importera YouTube-prenumerationer", + "Import Invidious data": "Importera Invidious JSON data", + "Import YouTube subscriptions": "Importera YouTube/OPML prenumerationer", "Import FreeTube subscriptions (.db)": "Importera FreeTube-prenumerationer (.db)", "Import NewPipe subscriptions (.json)": "Importera NewPipe-prenumerationer (.json)", "Import NewPipe data (.zip)": "Importera NewPipe-data (.zip)", "Export": "Exportera", "Export subscriptions as OPML": "Exportera prenumerationer som OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportera prenumerationer som OPML (för NewPipe och FreeTube)", - "Export data as JSON": "Exportera data som JSON", + "Export data as JSON": "Exportera Invidious data som JSON", "Delete account?": "Radera konto?", "History": "Historik", "An alternative front-end to YouTube": "Ett alternativt gränssnitt till YouTube", @@ -63,7 +63,7 @@ "preferences_related_videos_label": "Visa relaterade videor? ", "preferences_annotations_label": "Visa länkar-i-videon som förval? ", "preferences_extend_desc_label": "Förläng videobeskrivning automatiskt: ", - "preferences_vr_mode_label": "Interaktiva 360-gradervideos: ", + "preferences_vr_mode_label": "Interaktiva 360-gradervideos (kräver WebGL): ", "preferences_category_visual": "Visuella inställningar", "preferences_player_style_label": "Spelarstil: ", "Dark mode: ": "Mörkt läge: ", @@ -152,7 +152,7 @@ "View YouTube comments": "Visa YouTube-kommentarer", "View more comments on Reddit": "Visa flera kommentarer på Reddit", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Visa `x` kommentarer", + "([^.,0-9]|^)1([^.,0-9]|$)": "Visa `x` kommentar", "": "Visa `x` kommentarer" }, "View Reddit comments": "Visa Reddit-kommentarer", @@ -167,7 +167,7 @@ "Wrong username or password": "Ogiltigt användarnamn eller lösenord", "Password cannot be empty": "Lösenordet kan inte vara tomt", "Password cannot be longer than 55 characters": "Lösenordet kan inte vara längre än 55 tecken", - "Please log in": "Logga in", + "Please log in": "Snälla logga in", "Invidious Private Feed for `x`": "Ogiltig privat flöde för `x`", "channel:`x`": "kanal `x`", "Deleted or invalid channel": "Raderad eller ogiltig kanal", @@ -311,8 +311,8 @@ "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(redigerad)", "YouTube comment permalink": "Permanent YouTube-länk till innehållet", - "permalink": "permalänk", - "`x` marked it with a ❤": "`x` lämnade ett ❤", + "permalink": "permanent länk", + "`x` marked it with a ❤": "`x` markerade det med ett ❤", "Audio mode": "Ljudläge", "Video mode": "Videoläge", "channel_tab_videos_label": "Videor", @@ -320,30 +320,30 @@ "channel_tab_community_label": "Gemenskap", "search_filters_sort_option_relevance": "Relevans", "search_filters_sort_option_rating": "Rankning", - "search_filters_sort_option_date": "Datum", + "search_filters_sort_option_date": "Uppladdnings Datum", "search_filters_sort_option_views": "Visningar", "search_filters_type_label": "Typ", "search_filters_duration_label": "Varaktighet", "search_filters_features_label": "Funktioner", "search_filters_sort_label": "Sortera efter", - "search_filters_date_option_hour": "timme", - "search_filters_date_option_today": "idag", - "search_filters_date_option_week": "vecka", - "search_filters_date_option_month": "månad", - "search_filters_date_option_year": "år", - "search_filters_type_option_video": "video", - "search_filters_type_option_channel": "kanal", - "search_filters_type_option_playlist": "spellista", - "search_filters_type_option_movie": "film", - "search_filters_type_option_show": "tv-serie", - "search_filters_features_option_hd": "hd", - "search_filters_features_option_subtitles": "undertexter", - "search_filters_features_option_c_commons": "creative_commons", - "search_filters_features_option_three_d": "3d", - "search_filters_features_option_live": "live", - "search_filters_features_option_four_k": "4k", - "search_filters_features_option_location": "plats", - "search_filters_features_option_hdr": "hdr", + "search_filters_date_option_hour": "Senaste Timmen", + "search_filters_date_option_today": "Idag", + "search_filters_date_option_week": "Denna vecka", + "search_filters_date_option_month": "Denna månad", + "search_filters_date_option_year": "Detta år", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Kanal", + "search_filters_type_option_playlist": "Spellista", + "search_filters_type_option_movie": "Film", + "search_filters_type_option_show": "Serie", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Undertexter/CC", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Live", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Plats", + "search_filters_features_option_hdr": "HDR", "Current version: ": "Nuvarande version: ", "next_steps_error_message_refresh": "Uppdatera", "next_steps_error_message_go_to_youtube": "Gå till Youtube", @@ -352,5 +352,141 @@ "search_filters_duration_option_long": "Lång (> 20 minuter)", "footer_documentation": "Dokumentation", "search_filters_duration_option_short": "Kort (< 4 minuter)", - "search_filters_title": "Filter" + "search_filters_title": "Filter", + "Korean (auto-generated)": "Koreanska (auto-genererad)", + "search_filters_features_option_three_sixty": "360°", + "preferences_quality_dash_option_worst": "Sämst", + "channel_tab_podcasts_label": "Podcaster", + "preferences_save_player_pos_label": "Spara uppspelningsposition: ", + "Spanish (Mexico)": "Spanska (Mexiko)", + "preferences_region_label": "Innehållsland: ", + "generic_subscriptions_count": "{{count}} prenumeration", + "generic_subscriptions_count_plural": "{{count}} prenumerationer", + "search_filters_apply_button": "Använd valda filter", + "Download is disabled": "Nedladdning är inaktiverad", + "comments_points_count": "{{count}} poäng", + "comments_points_count_plural": "{{count}} poäng", + "preferences_quality_dash_option_2160p": "2160p", + "German (auto-generated)": "Tyska (auto-genererad)", + "Japanese (auto-generated)": "Japanska (auto-genererad)", + "preferences_quality_option_medium": "Medium", + "footer_donate_page": "Donera", + "search_message_change_filters_or_query": "Prova att bredda din sökfråga och/eller ändra filtren.", + "crash_page_before_reporting": "Innan du rapporterar en bugg, se till att du har:", + "preferences_quality_dash_option_best": "Bäst", + "Channel Sponsor": "Kanal Sponsor", + "generic_videos_count": "{{count}} video", + "generic_videos_count_plural": "{{count}} videor", + "videoinfo_started_streaming_x_ago": "Började sända `x` sedan", + "videoinfo_youTube_embed_link": "Bädda in", + "channel_tab_streams_label": "Livesändningar", + "playlist_button_add_items": "Lägg till videor", + "generic_count_minutes": "{{count}}minut", + "generic_count_minutes_plural": "{{count}}minuter", + "preferences_quality_dash_option_720p": "720p", + "preferences_watch_history_label": "Aktivera visningshistorik: ", + "user_saved_playlists": "`x` sparade spellistor", + "Spanish (Spain)": "Spanska (Spanien)", + "invidious": "Invidious", + "crash_page_refresh": "försökte <a href=\"`x`\">uppdatera sidan</a>", + "Chinese (Hong Kong)": "Kinesiska (Hong Kong)", + "Artist: ": "Artist: ", + "generic_count_months": "{{count}}månad", + "generic_count_months_plural": "{{count}}månader", + "search_message_use_another_instance": " Du kan också <a href=\"`x`\">söka på en annan instans</a>.", + "generic_subscribers_count": "{{count}} prenumerant", + "generic_subscribers_count_plural": "{{count}} prenumeranter", + "download_subtitles": "Undertexter - `x` (.vtt)", + "generic_button_save": "Spara", + "crash_page_search_issue": "sökte efter <a href=\"`x`\">befintliga problem på GitHub</a>", + "generic_button_cancel": "Avbryt", + "none": "ingen", + "English (United States)": "English (Förenta staterna)", + "subscriptions_unseen_notifs_count": "{{count}}osedd notifikation", + "subscriptions_unseen_notifs_count_plural": "{{count}}osedda notifikationer", + "Album: ": "Album: ", + "preferences_quality_option_dash": "DASH (adaptiv kvalitet)", + "preferences_quality_dash_option_1080p": "1080p", + "Video unavailable": "Video inte tillgänglig", + "tokens_count": "{{count}}nyckel", + "tokens_count_plural": "{{count}}nycklar", + "Chinese (China)": "Kinesiska (Kina)", + "Italian (auto-generated)": "Italienska (auto-genererad)", + "channel_tab_shorts_label": "Shorts", + "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_360p": "360p", + "search_message_no_results": "Inga resultat hittades.", + "channel_tab_releases_label": "Releaser", + "preferences_quality_dash_option_144p": "144p", + "Interlingue": "Interlingue (auto-genererad)", + "Song: ": "Låt: ", + "generic_channels_count": "{{count}} kanal", + "generic_channels_count_plural": "{{count}} kanaler", + "Chinese (Taiwan)": "Kinesiska (Taiwan)", + "preferences_quality_dash_label": "Önskad DASH-videokvalitet: ", + "adminprefs_modified_source_code_url_label": "URL till modifierad källkodslager", + "Turkish (auto-generated)": "Turkiska (auto-genererad)", + "Indonesian (auto-generated)": "Indonesiska (auto-genererad)", + "Portuguese (auto-generated)": "Portugisiska (auto-genererad)", + "generic_count_years": "{{count}}år", + "generic_count_years_plural": "{{count}}år", + "videoinfo_invidious_embed_link": "Bädda in länk", + "Popular enabled: ": "Populär aktiverad: ", + "Spanish (auto-generated)": "Spanska (auto-genererad)", + "preferences_quality_option_small": "Liten", + "English (United Kingdom)": "Engelska (Storbritannien)", + "channel_tab_playlists_label": "Spellistor", + "generic_button_edit": "Redigera", + "generic_playlists_count": "{{count}} spellista", + "generic_playlists_count_plural": "{{count}} spellistor", + "preferences_quality_option_hd720": "HD720p", + "search_filters_features_option_purchased": "Köpt", + "search_filters_date_option_none": "Vilket datum som helst", + "preferences_quality_dash_option_auto": "Auto", + "Cantonese (Hong Kong)": "Katonesiska (Hong Kong)", + "crash_page_report_issue": "Om inget av ovanstående hjälpte, vänligen <a href=\"`x`\">öppna ett nytt nummer på GitHub</a> (helst på engelska) och inkludera följande text i ditt meddelande (översätt INTE den texten):", + "crash_page_switch_instance": "försökte <a href=\"`x`\">använda en annan instans</a>", + "generic_count_weeks": "{{count}}vecka", + "generic_count_weeks_plural": "{{count}}veckor", + "videoinfo_watch_on_youTube": "Titta på YouTube", + "Music in this video": "Musik i denna video", + "footer_modfied_source_code": "Modifierad källkod", + "generic_button_rss": "RSS", + "preferences_quality_dash_option_4320p": "4320p", + "generic_count_hours": "{{count}}timme", + "generic_count_hours_plural": "{{count}}timmar", + "French (auto-generated)": "Franska (auto-genererad)", + "crash_page_read_the_faq": "läs <a href=\"`x`\">Vanliga frågor (FAQ)</a>", + "user_created_playlists": "`x` skapade spellistor", + "channel_tab_channels_label": "Kanaler", + "search_filters_type_option_all": "Vilken typ som helst", + "Russian (auto-generated)": "Ryska (auto-genererad)", + "preferences_quality_dash_option_480p": "480p", + "comments_view_x_replies": "Se {{count}} svar", + "comments_view_x_replies_plural": "Se {{count}} svar", + "footer_original_source_code": "Ursprunglig källkod", + "Portuguese (Brazil)": "Portugisiska (Brasilien)", + "search_filters_features_option_vr180": "VR180", + "error_video_not_in_playlist": "Den begärda videon finns inte i den här spellistan. <a href=\"`x`\">Klicka här för startsidan för spellistan.</a>", + "Dutch (auto-generated)": "Nederländska (auto-genererad)", + "generic_count_days": "{{count}}dag", + "generic_count_days_plural": "{{count}}dagar", + "Vietnamese (auto-generated)": "Vietnamesiska (auto-genererad)", + "search_filters_duration_option_none": "Vilken varaktighet som helst", + "preferences_quality_dash_option_240p": "240p", + "Chinese": "Kinesiska", + "preferences_automatic_instance_redirect_label": "Automatisk instansomdirigering (återgång till redirect.invidious.io): ", + "generic_button_delete": "Radera", + "Import YouTube playlist (.csv)": "Importera YouTube spellista (.csv)", + "next_steps_error_message": "Därefter bör du försöka: ", + "Standard YouTube license": "Standard YouTube licens", + "Import YouTube watch history (.json)": "Importera YouTube visningshistorik (.json)", + "search_filters_duration_option_medium": "Medium (4 - 20 minuter)", + "generic_count_seconds": "{{count}}sekund", + "generic_count_seconds_plural": "{{count}}sekunder", + "search_filters_date_label": "Uppladdningsdatum", + "crash_page_you_found_a_bug": "Det verkar som att du har hittat en bugg i Invidious!", + "generic_views_count": "{{count}} visning", + "generic_views_count_plural": "{{count}} visningar", + "toggle_theme": "Växla tema" } diff --git a/locales/tr.json b/locales/tr.json index 0575a4dd..d25cfd65 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -486,5 +486,7 @@ "playlist_button_add_items": "Video ekle", "channel_tab_podcasts_label": "Podcast'ler", "generic_channels_count": "{{count}} kanal", - "generic_channels_count_plural": "{{count}} kanal" + "generic_channels_count_plural": "{{count}} kanal", + "Import YouTube watch history (.json)": "YouTube İzleme Geçmişini İçe Aktar (.json)", + "toggle_theme": "Temayı Değiştir" } diff --git a/locales/uk.json b/locales/uk.json index c26618fe..f9640bba 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -503,5 +503,7 @@ "generic_button_save": "Зберегти", "generic_channels_count_0": "{{count}} канал", "generic_channels_count_1": "{{count}} канали", - "generic_channels_count_2": "{{count}} каналів" + "generic_channels_count_2": "{{count}} каналів", + "Import YouTube watch history (.json)": "Імпортувати історію переглядів YouTube (.json)", + "toggle_theme": "Перемкнути тему" } diff --git a/locales/vi.json b/locales/vi.json index 9cb87d3e..4f8dc30d 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -1,62 +1,62 @@ { "generic_videos_count_0": "{{count}} video", - "generic_subscribers_count_0": "{{count}} người theo dõi", + "generic_subscribers_count_0": "{{count}} người đăng ký", "LIVE": "TRỰC TIẾP", "Shared `x` ago": "Đã chia sẻ `x` trước", - "Unsubscribe": "Hủy theo dõi", - "Subscribe": "Theo dõi", + "Unsubscribe": "Hủy đăng ký", + "Subscribe": "Đăng ký", "View channel on YouTube": "Xem kênh trên YouTube", "View playlist on YouTube": "Xem danh sách phát trên YouTube", - "newest": "mới nhất", - "oldest": "lâu đời nhất", - "popular": "phổ biến", - "last": "Cuối cùng", + "newest": "Mới nhất", + "oldest": "Cũ nhất", + "popular": "Phổ biến", + "last": "cuối cùng", "Next page": "Trang tiếp theo", "Previous page": "Trang trước", "Clear watch history?": "Xóa lịch sử xem?", "New password": "Mật khẩu mới", "New passwords must match": "Mật khẩu mới phải khớp", "Authorize token?": "Cấp phép mã thông báo?", - "Authorize token for `x`?": "Cấp phép mã thông báo cho` x`?", - "Yes": "Đúng", + "Authorize token for `x`?": "Cấp phép mã thông báo cho `x`?", + "Yes": "Có", "No": "Không", "Import and Export Data": "Nhập và xuất dữ liệu", "Import": "Nhập", - "Import Invidious data": "Nhập dữ liệu Invidious JSON", - "Import YouTube subscriptions": "Nhập dữ liệu thuê bao YouTube/OPML", - "Import FreeTube subscriptions (.db)": "Nhập đăng ký FreeTube (.db)", - "Import NewPipe subscriptions (.json)": "Nhập đăng ký NewPipe (.json)", - "Import NewPipe data (.zip)": "Nhập dữ liệu NewPipe (.zip)", + "Import Invidious data": "Nhập dữ liệu Invidious dưới dạng JSON", + "Import YouTube subscriptions": "Nhập các kênh đã đăng ký từ YouTube/OPML", + "Import FreeTube subscriptions (.db)": "Nhập các kênh đã đăng ký từ FreeTube (.db)", + "Import NewPipe subscriptions (.json)": "Nhập các kênh đã đăng ký từ NewPipe (.json)", + "Import NewPipe data (.zip)": "Nhập dữ liệu từ NewPipe (.zip)", "Export": "Xuất", - "Export subscriptions as OPML": "Xuất đăng ký dưới dạng OPML", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "Xuất đăng ký dưới dạng OPML (cho NewPipe & FreeTube)", + "Export subscriptions as OPML": "Xuất các kênh đã đăng ký dưới dạng OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Xuất các kênh đã đăng ký dưới dạng OPML (cho NewPipe & FreeTube)", "Export data as JSON": "Xuất dữ liệu Invidious dưới dạng JSON", "Delete account?": "Xóa tài khoản?", "History": "Lịch sử", - "An alternative front-end to YouTube": "Giao diện người dùng thay thế cho YouTube", + "An alternative front-end to YouTube": "Một front-end thay thế cho YouTube", "JavaScript license information": "Thông tin giấy phép JavaScript", "source": "nguồn", "Log in": "Đăng nhập", "Log in/register": "Đăng nhập / đăng ký", - "User ID": "Tên người dùng", + "User ID": "ID người dùng", "Password": "Mật khẩu", - "Time (h:mm:ss):": "Thời gian (h: mm: ss):", - "Text CAPTCHA": "Nhắn tin tới CAPTCHA", - "Image CAPTCHA": "Hình ảnh CAPTCHA", + "Time (h:mm:ss):": "Thời gian (h:mm:ss):", + "Text CAPTCHA": "CAPTCHA dạng chữ", + "Image CAPTCHA": "CAPTCHA dạng ảnh", "Sign In": "Đăng nhập", "Register": "Đăng ký", "E-mail": "E-mail", "Preferences": "Sở thích", "preferences_category_player": "Tùy chọn trình phát video", "preferences_video_loop_label": "Luôn lặp lại: ", - "preferences_autoplay_label": "Tự chạy: ", + "preferences_autoplay_label": "Tự động phát: ", "preferences_continue_label": "Phát kế tiếp theo mặc định: ", "preferences_continue_autoplay_label": "Tự động phát video tiếp theo: ", "preferences_listen_label": "Nghe theo mặc định: ", "preferences_local_label": "Video proxy: ", "preferences_speed_label": "Tốc độ mặc định: ", "preferences_quality_label": "Chất lượng video ưa thích: ", - "preferences_volume_label": "Âm lượng trình phát video: ", + "preferences_volume_label": "Âm lượng video: ", "preferences_comments_label": "Nhận xét mặc định: ", "youtube": "YouTube", "reddit": "Reddit", @@ -64,7 +64,7 @@ "Fallback captions: ": "Phụ đề dự phòng: ", "preferences_related_videos_label": "Hiển thị các video có liên quan: ", "preferences_annotations_label": "Hiển thị chú thích theo mặc định: ", - "preferences_extend_desc_label": "Tự động mở rộng mô tả video: ", + "preferences_extend_desc_label": "Tự động mở rộng phần mô tả của video: ", "preferences_vr_mode_label": "Video 360 độ tương tác (yêu cầu WebGL): ", "preferences_category_visual": "Tùy chọn hình ảnh", "preferences_player_style_label": "Phong cách trình phát: ", @@ -82,24 +82,24 @@ "preferences_sort_label": "Sắp xếp video theo: ", "published": "được phát hành", "published - reverse": "đã xuất bản - đảo ngược", - "alphabetically": "theo thứ tự bảng chữ cái", - "alphabetically - reverse": "theo thứ tự bảng chữ cái - đảo ngược", - "channel name": "Tên kênh", - "channel name - reverse": "tên kênh - đảo ngược", + "alphabetically": "Thứ tự (A - Z)", + "alphabetically - reverse": "Thứ tự (Z - A)", + "channel name": "Tên kênh (A - Z)", + "channel name - reverse": "Tên kênh (Z - A)", "Only show latest video from channel: ": "Chỉ hiển thị video mới nhất từ kênh: ", "Only show latest unwatched video from channel: ": "Chỉ hiển thị video chưa xem mới nhất từ kênh: ", - "preferences_unseen_only_label": "Chỉ hiển thị chưa xem: ", + "preferences_unseen_only_label": "Chỉ hiển thị các video chưa từng xem: ", "preferences_notifications_only_label": "Chỉ hiển thị thông báo (nếu có): ", "Enable web notifications": "Bật thông báo web", - "`x` uploaded a video": "` x` đã tải lên một video", - "`x` is live": "` x` đang phát trực tiếp", + "`x` uploaded a video": "`x` đã tải lên một video", + "`x` is live": "`x` đang phát trực tiếp", "preferences_category_data": "Tùy chọn dữ liệu", "Clear watch history": "Xóa lịch sử xem", "Import/export data": "Nhập / xuất dữ liệu", "Change password": "Đổi mật khẩu", "Manage subscriptions": "Quản lý các mục đăng kí", "Manage tokens": "Quản lý mã thông báo", - "Watch history": "Lịch sử xem", + "Watch history": "Xem lịch sử", "Delete account": "Xóa tài khoản", "preferences_category_admin": "Tùy chọn quản trị viên", "preferences_default_home_label": "Trang chủ mặc định: ", @@ -121,7 +121,7 @@ "View privacy policy.": "Xem chính sách bảo mật.", "Trending": "Xu hướng", "Public": "Công khai", - "Unlisted": "Không hiển thị", + "Unlisted": "Không công khai", "Private": "Riêng tư", "View all playlists": "Xem tất cả danh sách phát", "Updated `x` ago": "Đã cập nhật` x` trước", @@ -131,24 +131,24 @@ "Title": "Tiêu đề", "Playlist privacy": "Bảo mật danh sách phát", "Editing playlist `x`": "Chỉnh sửa danh sách phát` x`", - "Show more": "Cho xem nhiều hơn", - "Show less": "Hiện ít hơn", + "Show more": "Hiển thị thêm", + "Show less": "Hiển thị ít hơn", "Watch on YouTube": "Xem trên YouTube", "Switch Invidious Instance": "Chuyển phiên bản Invidious", "Hide annotations": "Ẩn chú thích", "Show annotations": "Hiển thị chú thích", "Genre: ": "Thể loại: ", "License: ": "Giấy phép: ", - "Family friendly? ": "Gia đình thân thiện? ", + "Family friendly? ": "Thân thiện với gia đình? ", "Wilson score: ": "Điểm số Wilson: ", "Engagement: ": "Hôn ước: ", "Whitelisted regions: ": "Các vùng nằm trong danh sách trắng: ", - "Blacklisted regions: ": "Khu vực nằm trong danh sách đen: ", + "Blacklisted regions: ": "Các vùng nằm trong danh sách đen: ", "Shared `x`": "Chia sẻ` x`", - "View Reddit comments": "Xem nhận xét trên Reddit", - "Hide replies": "Ẩn câu trả lời", - "Show replies": "Hiển thị câu trả lời", - "Incorrect password": "Mật khẩu không đúng", + "View Reddit comments": "Xem bình luận trên Reddit", + "Hide replies": "Ẩn phản hồi", + "Show replies": "Hiển thị phản hồi", + "Incorrect password": "Mật khẩu không chính xác", "Wrong answer": "Câu trả lời sai", "Erroneous CAPTCHA": "CAPTCHA bị lỗi", "CAPTCHA is a required field": "CAPTCHA là trường bắt buộc", @@ -190,35 +190,35 @@ "Bulgarian": "Tiếng Bungari", "Burmese": "Tiếng Miến Điện", "Catalan": "Tiếng Catalan", - "Cebuano": "Cebuano", + "Cebuano": "Tiếng Cebu", "Chinese (Simplified)": "Tiếng Trung (Giản thể)", "Chinese (Traditional)": "Tiếng Trung (Phồn thể)", - "Corsican": "Corsican", + "Corsican": "Tiếng Corse", "Croatian": "Tiếng Croatia", "Czech": "Tiếng Séc", - "Danish": "Người Đan Mạch", + "Danish": "Tiếng Đan Mạch", "Dutch": "Tiếng Hà Lan", "Esperanto": "Quốc tế ngữ", "Estonian": "Tiếng Estonia", - "Filipino": "Filipino", + "Filipino": "Tiếng Philippines", "Finnish": "Tiếng Phần Lan", - "French": "Người Pháp", + "French": "Tiếng Pháp", "Galician": "Tiếng Galicia", "Georgian": "Tiếng Georgia", "German": "Tiếng Đức", - "Greek": "Người Hy Lạp", - "Gujarati": "Gujarati", - "Haitian Creole": "Tiếng Creole của Haiti", - "Hausa": "Hausa", + "Greek": "Tiếng Hy Lạp", + "Gujarati": "Tiếng Gujarat", + "Haitian Creole": "Tiếng Creole (Haiti)", + "Hausa": "Tiếng Hausa", "Hawaiian": "Tiếng Hawaii", "Hebrew": "Tiếng Do Thái", "Hindi": "Tiếng Hindi", - "Hmong": "Hmong", - "Hungarian": "Người Hungary", + "Hmong": "Tiếng Hmong", + "Hungarian": "Tiếng Hungary", "Icelandic": "Tiếng Iceland", - "Igbo": "Igbo", + "Igbo": "Tiếng Igbo", "Indonesian": "Tiếng Indonesia", - "Irish": "Tiếng Ailen", + "Irish": "Tiếng Ireland", "Italian": "Tiếng Ý", "Japanese": "Tiếng Nhật", "Javanese": "Tiếng Java", @@ -237,37 +237,37 @@ "Malagasy": "Tiếng Malagasy", "Malay": "Tiếng Mã Lai", "Malayalam": "Tiếng Malayalam", - "Maltese": "Cây nho", + "Maltese": "Tiếng Malta", "Maori": "Tiếng Maori", - "Marathi": "Marathi", + "Marathi": "Tiếng Marathi", "Mongolian": "Tiếng Mông Cổ", "Nepali": "Tiếng Nepal", - "Norwegian Bokmål": "Tiếng Na Uy Bokmål", - "Nyanja": "Nyanja", - "Pashto": "Pashto", + "Norwegian Bokmål": "Tiếng Na Uy (Bokmål)", + "Nyanja": "Tiếng Chewa / Nyanja", + "Pashto": "Tiếng Pashtun", "Persian": "Tiếng Ba Tư", - "Polish": "Đánh bóng", + "Polish": "Tiếng Ba Lan", "Portuguese": "Tiếng Bồ Đào Nha", - "Punjabi": "Punjabi", + "Punjabi": "Tiếng Punjab", "Romanian": "Tiếng Rumani", "Russian": "Tiếng Nga", - "Samoan": "Samoan", - "Scottish Gaelic": "Tiếng Gaelic Scotland", + "Samoan": "Tiếng Samoa", + "Scottish Gaelic": "Tiếng Gaelic (Scotland)", "Serbian": "Tiếng Serbia", - "Shona": "Shona", - "Sindhi": "Sindhi", - "Sinhala": "Sinhala", + "Shona": "Tiếng Shona", + "Sindhi": "Tiếng Sindh", + "Sinhala": "Tiếng Sinhala", "Slovak": "Tiếng Slovak", "Slovenian": "Tiếng Slovenia", "Somali": "Tiếng Somali", "Southern Sotho": "Southern Sotho", - "Spanish": "Người Tây Ban Nha", + "Spanish": "Tiếng Tây Ban Nha", "Spanish (Latin America)": "Tiếng Tây Ban Nha (Mỹ Latinh)", "Sundanese": "Tiếng Sundan", "Swahili": "Tiếng Swahili", "Swedish": "Tiếng Thụy Điển", - "Tajik": "Tajik", - "Tamil": "Tamil", + "Tajik": "Tiếng Tajik", + "Tamil": "Tiếng Tamil", "Telugu": "Tiếng Telugu", "Thai": "Tiếng Thái", "Turkish": "Tiếng Thổ Nhĩ Kỳ", @@ -275,17 +275,17 @@ "Urdu": "Tiếng Urdu", "Uzbek": "Tiếng Uzbek", "Vietnamese": "Tiếng Việt", - "Welsh": "Người xứ Wales", - "Western Frisian": "Western Frisian", - "Xhosa": "Xhosa", - "Yiddish": "Yiddish", - "Yoruba": "Yoruba", + "Welsh": "Tiếng Wales", + "Western Frisian": "Tiếng Tây Frisia", + "Xhosa": "Tiếng Nam Phi", + "Yiddish": "Tiếng Yiddish", + "Yoruba": "Tiếng Yoruba", "Zulu": "Tiếng Zulu", "Fallback comments: ": "Nhận xét dự phòng: ", "Popular": "Phổ biến", "Search": "Tìm kiếm", "Top": "Hàng đầu", - "About": "Trong khoảng", + "About": "Giới thiệu", "Rating: ": "Xếp hạng: ", "preferences_locale_label": "Ngôn ngữ: ", "View as playlist": "Xem dưới dạng danh sách phát", @@ -295,45 +295,45 @@ "News": "Tin tức", "Movies": "Phim", "Download": "Tải xuống", - "Download as: ": "Tải tệp dưới dạng: ", + "Download as: ": "Tải xuống dưới dạng: ", "%A %B %-d, %Y": "% A% B% -d,% Y", "(edited)": "(đã chỉnh sửa)", "YouTube comment permalink": "Liên kết cố định nhận xét trên YouTube", "permalink": "liên kết cố định", "`x` marked it with a ❤": "` x` đã đánh dấu nó bằng một ❤", - "Audio mode": "Chế độ âm thanh", - "Video mode": "Chế độ quay", + "Audio mode": "Chế độ audio", + "Video mode": "Chế độ video", "channel_tab_videos_label": "Video", "Playlists": "Danh sách phát", "channel_tab_community_label": "Cộng đồng", - "search_filters_sort_option_relevance": "liên quan", + "search_filters_sort_option_relevance": "Liên quan", "search_filters_sort_option_rating": "Xếp hạng", - "search_filters_sort_option_date": "ngày", - "search_filters_sort_option_views": "lượt xem", - "search_filters_type_label": "content_type", - "search_filters_duration_label": "thời lượng", - "search_filters_features_label": "đặc trưng", - "search_filters_sort_label": "sắp xếp", - "search_filters_date_option_hour": "giờ", - "search_filters_date_option_today": "hôm nay", - "search_filters_date_option_week": "tuần", - "search_filters_date_option_month": "tháng", - "search_filters_date_option_year": "năm", + "search_filters_sort_option_date": "Ngày tải lên", + "search_filters_sort_option_views": "Lượt xem", + "search_filters_type_label": "Thể loại", + "search_filters_duration_label": "Thời lượng", + "search_filters_features_label": "Đặc điểm", + "search_filters_sort_label": "Sắp xếp theo", + "search_filters_date_option_hour": "Một giờ qua", + "search_filters_date_option_today": "Hôm nay", + "search_filters_date_option_week": "Tuần này", + "search_filters_date_option_month": "Tháng này", + "search_filters_date_option_year": "Năm này", "search_filters_type_option_video": "video", - "search_filters_type_option_channel": "kênh", - "search_filters_type_option_playlist": "danh sách phát", - "search_filters_type_option_movie": "bộ phim", - "search_filters_type_option_show": "chỉ", - "search_filters_features_option_hd": "hd", - "search_filters_features_option_subtitles": "phụ đề", - "search_filters_features_option_c_commons": "Commons sáng tạo", - "search_filters_features_option_three_d": "3d", - "search_filters_features_option_live": "trực tiếp", - "search_filters_features_option_four_k": "4k", - "search_filters_features_option_location": "vị trí", - "search_filters_features_option_hdr": "hdr", + "search_filters_type_option_channel": "Kênh", + "search_filters_type_option_playlist": "Danh sách phát", + "search_filters_type_option_movie": "Phim", + "search_filters_type_option_show": "Hiện", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Phụ đề", + "search_filters_features_option_c_commons": "Giấy phép Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Trực tiếp", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Vị trí", + "search_filters_features_option_hdr": "HDR", "Current version: ": "Phiên bản hiện tại: ", - "search_filters_title": "bộ lọc", + "search_filters_title": "Bộ lọc", "generic_playlists_count": "{{count}} danh sách phát", "generic_views_count": "{{count}} lượt xem", "View `x` comments": { @@ -350,31 +350,31 @@ "preferences_quality_dash_label": "Chất lượng video DASH ưa thích ", "preferences_quality_dash_option_auto": "Tự động", "Subscriptions": "Thuê bao", - "View YouTube comments": "Hiển thị bình luận trên YouTube", + "View YouTube comments": "Hiển thị bình luận từ YouTube", "View more comments on Reddit": "Hiển thị thêm bình luận từ Reddit", "Music in this video": "Nhạc trong video này", "Artist: ": "Nghệ sĩ: ", "Premieres `x`": "Phát lần đầu `x`", "preferences_region_label": "Nội dung theo quốc gia ", "search_message_change_filters_or_query": "Thử mở rộng nội dung tìm kiếm hoặc thay đổi bộ lọc.", - "preferences_quality_option_small": "Nhỏ", + "preferences_quality_option_small": "Thấp", "preferences_quality_dash_option_144p": "144p", "invidious": "Invidious", "preferences_quality_dash_option_240p": "240p", - "Import/export": "Xuất/nhập dữ liệu", - "preferences_quality_dash_option_4320p": "4320p", + "Import/export": "Nhập/Xuất", + "preferences_quality_dash_option_4320p": "4320p (8K)", "preferences_quality_option_dash": "DASH (tự tối ưu chất lượng)", "generic_subscriptions_count_0": "{{count}} người đăng kí", - "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_1440p": "1440p (2K)", "preferences_quality_dash_option_480p": "480p", - "preferences_quality_dash_option_2160p": "2160p", + "preferences_quality_dash_option_2160p": "2160p (4K)", "search_message_no_results": "Tìm kiếm không có kết quả.", "preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_720p": "720p", "preferences_quality_option_medium": "Trung bình", - "Load more": "Hiển thị thêm", + "Load more": "Tải thêm", "comments_points_count_0": "{{count}} điểm", - "Import YouTube playlist (.csv)": "Nhập danh sách phát YouTube (.csv)", + "Import YouTube playlist (.csv)": "Nhập các danh sách phát từ YouTube (.csv)", "preferences_quality_dash_option_best": "Tốt nhất", "preferences_quality_dash_option_360p": "360p", "subscriptions_unseen_notifs_count_0": "{{count}} thông báo chưa đọc", @@ -382,10 +382,93 @@ "search_message_use_another_instance": " Bạn cũng có thể tìm kiếm <a href=\"`x`\"> ở một phiên bản khác</a>.", "Standard YouTube license": "Giấy phép YouTube thông thường", "Album: ": "Album: ", - "preferences_save_player_pos_label": "Lưu vị trí xem cuối cùng ", + "preferences_save_player_pos_label": "Lưu vị trí xem: ", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn.", "Chinese (China)": "Tiếng Trung (Trung Quốc)", "generic_button_cancel": "Hủy", "Chinese": "Tiếng Trung", - "generic_button_delete": "Xóa" + "generic_button_delete": "Xóa", + "Korean (auto-generated)": "Tiếng Hàn (được tạo tự động)", + "search_filters_features_option_three_sixty": "360°", + "channel_tab_podcasts_label": "Podcast", + "Spanish (Mexico)": "Tiếng Tây Ban Nha (Mexico)", + "search_filters_apply_button": "Áp dụng các mục đã chọn", + "Download is disabled": "Tải xuống đã bị vô hiệu hóa.", + "next_steps_error_message_go_to_youtube": "Đi đến YouTube", + "German (auto-generated)": "Tiếng Đức (được tạo tự động)", + "Japanese (auto-generated)": "Tiếng Nhật (được tạo tự động)", + "footer_donate_page": "Ủng hộ", + "crash_page_before_reporting": "Trước khi báo cáo lỗi, hãy chắc chắn rằng bạn đã:", + "Channel Sponsor": "Nhà tài trợ của kênh", + "videoinfo_started_streaming_x_ago": "Đã bắt đầu phát sóng `x` trước", + "videoinfo_youTube_embed_link": "Nhúng", + "channel_tab_streams_label": "Phát trực tiếp", + "playlist_button_add_items": "Thêm video", + "generic_count_minutes_0": "{{count}} phút", + "user_saved_playlists": "`x` danh sách phát đã lưu", + "Spanish (Spain)": "Tiếng Tây Ban Nha (Tây Ban Nha)", + "crash_page_refresh": "Đã thử <a href=\"`x`\">tải lại trang</a>", + "Chinese (Hong Kong)": "Tiếng Trung (Hồng Kông)", + "generic_count_months_0": "{{count}} tháng", + "download_subtitles": "Phụ đề - `x` (.vtt)", + "generic_button_save": "Lưu", + "crash_page_search_issue": "Tìm <a href=\"`x`\">lỗi có sẵn trên GitHub</a>", + "none": "không", + "English (United States)": "Tiếng Anh (Mỹ)", + "next_steps_error_message_refresh": "Tải lại", + "Video unavailable": "Video không có sẵn", + "footer_source_code": "Mã nguồn", + "search_filters_duration_option_short": "Ngắn (< 4 phút)", + "search_filters_duration_option_long": "Dài (> 20 phút)", + "tokens_count_0": "{{count}} mã thông báo", + "Italian (auto-generated)": "Tiếng Ý (được tạo tự động)", + "channel_tab_shorts_label": "Shorts", + "channel_tab_releases_label": "Mới tải lên", + "`x` ago": "`x` trước", + "Interlingue": "Tiếng Khoa học Quốc tế", + "generic_channels_count_0": "{{count}} kênh", + "Chinese (Taiwan)": "Tiếng Trung (Đài Loan)", + "adminprefs_modified_source_code_url_label": "URL tới kho lưu trữ mã nguồn đã sửa đổi", + "Turkish (auto-generated)": "Tiếng Thổ Nhĩ Kỳ (được tạo tự động)", + "Indonesian (auto-generated)": "Tiếng Indonesia (được tạo tự động)", + "Portuguese (auto-generated)": "Tiếng Bồ Đào Nha (được tạo tự động)", + "generic_count_years_0": "{{count}} năm", + "videoinfo_invidious_embed_link": "Liên kết nhúng", + "Popular enabled: ": "Đã bật phổ biến: ", + "Spanish (auto-generated)": "Tiếng Tây Ban Nha (được tạo tự động)", + "English (United Kingdom)": "Tiếng Anh Anh", + "channel_tab_playlists_label": "Danh sách phát", + "generic_button_edit": "Sửa", + "search_filters_features_option_purchased": "Đã mua", + "search_filters_date_option_none": "Mọi thời điểm", + "Cantonese (Hong Kong)": "Tiếng Quảng Châu (Hồng Kông)", + "crash_page_report_issue": "Nếu các điều trên không giúp được, xin hãy <a href=\"`x`\">tạo vấn đề mới trên GitHub</a> (ưu tiên tiếng Anh) và đính kèm đoạn chữ sau trong nội dung (giữ nguyên KHÔNG dịch):", + "crash_page_switch_instance": "Đã thử <a href=\"`x`\">dùng một phiên bản khác</a>", + "generic_count_weeks_0": "{{count}} tuần", + "videoinfo_watch_on_youTube": "Xem trên YouTube", + "footer_modfied_source_code": "Mã nguồn đã chỉnh sửa", + "generic_button_rss": "RSS", + "generic_count_hours_0": "{{count}} giờ", + "French (auto-generated)": "Tiếng Pháp (được tạo tự động)", + "crash_page_read_the_faq": "Đọc <a href=\"`x`\">Hỏi đáp thường gặp (FAQ)</a>", + "user_created_playlists": "`x` danh sách phát đã tạo", + "channel_tab_channels_label": "Kênh", + "search_filters_type_option_all": "Mọi thể loại", + "Russian (auto-generated)": "Tiếng Nga (được tạo tự động)", + "comments_view_x_replies_0": "Xem {{count}} lượt trả lời", + "footer_original_source_code": "Mã nguồn gốc", + "Portuguese (Brazil)": "Tiếng Bồ Đào Nha (Brazil)", + "search_filters_features_option_vr180": "VR180", + "error_video_not_in_playlist": "Video không tồn tại trong danh sách phát. <a href=\"`x`\">Bấm để trở về trang chủ của danh sách phát.</a>", + "Dutch (auto-generated)": "Tiếng Hà Lan (được tạo tự động)", + "generic_count_days_0": "{{count}} ngày", + "Vietnamese (auto-generated)": "Tiếng Việt (được tạo tự động)", + "search_filters_duration_option_none": "Mọi thời lượng", + "footer_documentation": "Tài liệu", + "next_steps_error_message": "Bạn có thể thử: ", + "Import YouTube watch history (.json)": "Nhập lịch sử xem từ YouTube (.json)", + "search_filters_duration_option_medium": "Trung bình (4 - 20 phút)", + "generic_count_seconds_0": "{{count}} giây", + "search_filters_date_label": "Ngày tải lên", + "crash_page_you_found_a_bug": "Có vẻ như bạn đã tìm ra lỗi trong Indivious!" } diff --git a/locales/zh-CN.json b/locales/zh-CN.json index db86a9bf..faa67e6c 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -470,5 +470,6 @@ "generic_button_save": "保存", "generic_button_rss": "RSS", "channel_tab_releases_label": "公告", - "generic_channels_count_0": "{{count}} 个频道" + "generic_channels_count_0": "{{count}} 个频道", + "toggle_theme": "切换主题" } diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 565f1d88..1520c269 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -470,5 +470,6 @@ "playlist_button_add_items": "新增影片", "channel_tab_podcasts_label": "Podcast", "channel_tab_releases_label": "發布", - "generic_channels_count_0": "{{count}} 個頻道" + "generic_channels_count_0": "{{count}} 個頻道", + "toggle_theme": "切換佈景主題" } diff --git a/spec/i18next_plurals_spec.cr b/spec/i18next_plurals_spec.cr index dab97710..dcd0f5ec 100644 --- a/spec/i18next_plurals_spec.cr +++ b/spec/i18next_plurals_spec.cr @@ -17,7 +17,7 @@ FORM_TESTS = { "cy" => I18next::Plurals::PluralForms::Special_Welsh, "fr" => I18next::Plurals::PluralForms::Special_French_Portuguese, "en" => I18next::Plurals::PluralForms::Single_not_one, - "es" => I18next::Plurals::PluralForms::Single_not_one, + "es" => I18next::Plurals::PluralForms::Special_Spanish_Italian, "ga" => I18next::Plurals::PluralForms::Special_Irish, "gd" => I18next::Plurals::PluralForms::Special_Scottish_Gaelic, "he" => I18next::Plurals::PluralForms::Special_Hebrew, @@ -33,7 +33,8 @@ FORM_TESTS = { "mt" => I18next::Plurals::PluralForms::Special_Maltese, "or" => I18next::Plurals::PluralForms::Special_Odia, "pl" => I18next::Plurals::PluralForms::Special_Polish_Kashubian, - "pt" => I18next::Plurals::PluralForms::Single_gt_one, + "pt" => I18next::Plurals::PluralForms::Special_French_Portuguese, + "pt-PT" => I18next::Plurals::PluralForms::Single_gt_one, "pt-BR" => I18next::Plurals::PluralForms::Special_French_Portuguese, "ro" => I18next::Plurals::PluralForms::Special_Romanian, "sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak, @@ -77,10 +78,10 @@ SUFFIX_TESTS = { {num: 10, suffix: "_plural"}, ], "es" => [ - {num: 0, suffix: "_plural"}, - {num: 1, suffix: ""}, - {num: 10, suffix: "_plural"}, - {num: 6_000_000, suffix: "_plural"}, + {num: 0, suffix: "_2"}, + {num: 1, suffix: "_0"}, + {num: 10, suffix: "_2"}, + {num: 6_000_000, suffix: "_1"}, ], "fr" => [ {num: 0, suffix: "_0"}, diff --git a/spec/invidious/search/yt_filters_spec.cr b/spec/invidious/search/yt_filters_spec.cr index bf7f21e7..8abed5ce 100644 --- a/spec/invidious/search/yt_filters_spec.cr +++ b/spec/invidious/search/yt_filters_spec.cr @@ -12,45 +12,45 @@ end # page of Youtube with any browser devtools HTML inspector. DATE_FILTERS = { - Invidious::Search::Filters::Date::Hour => "EgIIAQ%3D%3D", - Invidious::Search::Filters::Date::Today => "EgIIAg%3D%3D", - Invidious::Search::Filters::Date::Week => "EgIIAw%3D%3D", - Invidious::Search::Filters::Date::Month => "EgIIBA%3D%3D", - Invidious::Search::Filters::Date::Year => "EgIIBQ%3D%3D", + Invidious::Search::Filters::Date::Hour => "EgIIAfABAQ%3D%3D", + Invidious::Search::Filters::Date::Today => "EgIIAvABAQ%3D%3D", + Invidious::Search::Filters::Date::Week => "EgIIA_ABAQ%3D%3D", + Invidious::Search::Filters::Date::Month => "EgIIBPABAQ%3D%3D", + Invidious::Search::Filters::Date::Year => "EgIIBfABAQ%3D%3D", } TYPE_FILTERS = { - Invidious::Search::Filters::Type::Video => "EgIQAQ%3D%3D", - Invidious::Search::Filters::Type::Channel => "EgIQAg%3D%3D", - Invidious::Search::Filters::Type::Playlist => "EgIQAw%3D%3D", - Invidious::Search::Filters::Type::Movie => "EgIQBA%3D%3D", + Invidious::Search::Filters::Type::Video => "EgIQAfABAQ%3D%3D", + Invidious::Search::Filters::Type::Channel => "EgIQAvABAQ%3D%3D", + Invidious::Search::Filters::Type::Playlist => "EgIQA_ABAQ%3D%3D", + Invidious::Search::Filters::Type::Movie => "EgIQBPABAQ%3D%3D", } DURATION_FILTERS = { - Invidious::Search::Filters::Duration::Short => "EgIYAQ%3D%3D", - Invidious::Search::Filters::Duration::Medium => "EgIYAw%3D%3D", - Invidious::Search::Filters::Duration::Long => "EgIYAg%3D%3D", + Invidious::Search::Filters::Duration::Short => "EgIYAfABAQ%3D%3D", + Invidious::Search::Filters::Duration::Medium => "EgIYA_ABAQ%3D%3D", + Invidious::Search::Filters::Duration::Long => "EgIYAvABAQ%3D%3D", } FEATURE_FILTERS = { - Invidious::Search::Filters::Features::Live => "EgJAAQ%3D%3D", - Invidious::Search::Filters::Features::FourK => "EgJwAQ%3D%3D", - Invidious::Search::Filters::Features::HD => "EgIgAQ%3D%3D", - Invidious::Search::Filters::Features::Subtitles => "EgIoAQ%3D%3D", - Invidious::Search::Filters::Features::CCommons => "EgIwAQ%3D%3D", - Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AQ%3D%3D", - Invidious::Search::Filters::Features::VR180 => "EgPQAQE%3D", - Invidious::Search::Filters::Features::ThreeD => "EgI4AQ%3D%3D", - Invidious::Search::Filters::Features::HDR => "EgPIAQE%3D", - Invidious::Search::Filters::Features::Location => "EgO4AQE%3D", - Invidious::Search::Filters::Features::Purchased => "EgJIAQ%3D%3D", + Invidious::Search::Filters::Features::Live => "EgJAAfABAQ%3D%3D", + Invidious::Search::Filters::Features::FourK => "EgJwAfABAQ%3D%3D", + Invidious::Search::Filters::Features::HD => "EgIgAfABAQ%3D%3D", + Invidious::Search::Filters::Features::Subtitles => "EgIoAfABAQ%3D%3D", + Invidious::Search::Filters::Features::CCommons => "EgIwAfABAQ%3D%3D", + Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AfABAQ%3D%3D", + Invidious::Search::Filters::Features::VR180 => "EgPQAQHwAQE%3D", + Invidious::Search::Filters::Features::ThreeD => "EgI4AfABAQ%3D%3D", + Invidious::Search::Filters::Features::HDR => "EgPIAQHwAQE%3D", + Invidious::Search::Filters::Features::Location => "EgO4AQHwAQE%3D", + Invidious::Search::Filters::Features::Purchased => "EgJIAfABAQ%3D%3D", } SORT_FILTERS = { - Invidious::Search::Filters::Sort::Relevance => "", - Invidious::Search::Filters::Sort::Date => "CAI%3D", - Invidious::Search::Filters::Sort::Views => "CAM%3D", - Invidious::Search::Filters::Sort::Rating => "CAE%3D", + Invidious::Search::Filters::Sort::Relevance => "8AEB", + Invidious::Search::Filters::Sort::Date => "CALwAQE%3D", + Invidious::Search::Filters::Sort::Views => "CAPwAQE%3D", + Invidious::Search::Filters::Sort::Rating => "CAHwAQE%3D", } Spectator.describe Invidious::Search::Filters do diff --git a/src/invidious.cr b/src/invidious.cr index e0bd0101..c8cac80e 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -217,7 +217,6 @@ public_folder "assets" Kemal.config.powered_by_header = false add_handler FilteredCompressHandler.new -add_handler APIHandler.new add_handler AuthHandler.new add_handler DenyFrame.new add_context_storage_type(Array(String)) diff --git a/src/invidious/database/statistics.cr b/src/invidious/database/statistics.cr index 1df549e2..9e4963fd 100644 --- a/src/invidious/database/statistics.cr +++ b/src/invidious/database/statistics.cr @@ -15,7 +15,7 @@ module Invidious::Database::Statistics PG_DB.query_one(request, as: Int64) end - def count_users_active_1m : Int64 + def count_users_active_6m : Int64 request = <<-SQL SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months' @@ -24,7 +24,7 @@ module Invidious::Database::Statistics PG_DB.query_one(request, as: Int64) end - def count_users_active_6m : Int64 + def count_users_active_1m : Int64 request = <<-SQL SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month' diff --git a/src/invidious/frontend/comments_reddit.cr b/src/invidious/frontend/comments_reddit.cr index b5647bae..4dda683e 100644 --- a/src/invidious/frontend/comments_reddit.cr +++ b/src/invidious/frontend/comments_reddit.cr @@ -33,7 +33,7 @@ module Invidious::Frontend::Comments <a href="javascript:void(0)" data-onclick="toggle_parent">[ − ]</a> <b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b> #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} - <span title="#{child.created_utc.to_s(translate(locale, "%a %B %-d %T %Y UTC"))}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span> + <span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span> <a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a> </p> <div> diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr index d140a858..cece289b 100644 --- a/src/invidious/helpers/handlers.cr +++ b/src/invidious/helpers/handlers.cr @@ -134,74 +134,6 @@ class AuthHandler < Kemal::Handler end end -class APIHandler < Kemal::Handler - {% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %} - only ["/api/v1/*"], {{method}} - {% end %} - exclude ["/api/v1/auth/notifications"], "GET" - exclude ["/api/v1/auth/notifications"], "POST" - - def call(env) - return call_next env unless only_match? env - - env.response.headers["Access-Control-Allow-Origin"] = "*" - - # Since /api/v1/notifications is an event-stream, we don't want - # to wrap the response - return call_next env if exclude_match? env - - # Here we swap out the socket IO so we can modify the response as needed - output = env.response.output - env.response.output = IO::Memory.new - - begin - call_next env - - env.response.output.rewind - - if env.response.output.as(IO::Memory).size != 0 && - env.response.headers.includes_word?("Content-Type", "application/json") - response = JSON.parse(env.response.output) - - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - - if env.params.query["pretty"]?.try &.== "1" - response = response.to_pretty_json - else - response = response.to_json - end - else - response = env.response.output.gets_to_end - end - rescue ex - env.response.content_type = "application/json" if env.response.headers.includes_word?("Content-Type", "text/html") - env.response.status_code = 500 - - if env.response.headers.includes_word?("Content-Type", "application/json") - response = {"error" => ex.message || "Unspecified error"} - - if env.params.query["pretty"]?.try &.== "1" - response = response.to_pretty_json - else - response = response.to_json - end - end - ensure - env.response.output = output - env.response.print response - - env.response.flush - end - end -end - class DenyFrame < Kemal::Handler exclude ["/embed/*"] diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 6dc9860e..6add0237 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -78,15 +78,6 @@ def create_notification_stream(env, topics, connection_channel) video.published = published response = JSON.parse(video.to_json(locale, nil)) - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - env.response.puts "id: #{id}" env.response.puts "data: #{response.to_json}" env.response.puts @@ -113,15 +104,6 @@ def create_notification_stream(env, topics, connection_channel) Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video| response = JSON.parse(video.to_json(locale)) - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - env.response.puts "id: #{id}" env.response.puts "data: #{response.to_json}" env.response.puts @@ -155,15 +137,6 @@ def create_notification_stream(env, topics, connection_channel) video.published = Time.unix(published) response = JSON.parse(video.to_json(locale, nil)) - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - env.response.puts "id: #{id}" env.response.puts "data: #{response.to_json}" env.response.puts diff --git a/src/invidious/helpers/i18next.cr b/src/invidious/helpers/i18next.cr index 252af6b9..9f4077e1 100644 --- a/src/invidious/helpers/i18next.cr +++ b/src/invidious/helpers/i18next.cr @@ -47,19 +47,19 @@ module I18next::Plurals private PLURAL_SETS = { PluralForms::Single_gt_one => [ - "ach", "ak", "am", "arn", "br", "fil", "gun", "ln", "mfe", "mg", - "mi", "oc", "pt", "tg", "tl", "ti", "tr", "uz", "wa", + "ach", "ak", "am", "arn", "br", "fa", "fil", "gun", "ln", "mfe", "mg", + "mi", "oc", "pt-PT", "tg", "tl", "ti", "tr", "uz", "wa", ], PluralForms::Single_not_one => [ "af", "an", "ast", "az", "bg", "bn", "ca", "da", "de", "dev", "el", "en", - "eo", "es", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi", + "eo", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi", "hu", "hy", "ia", "kk", "kn", "ku", "lb", "mai", "ml", "mn", "mr", "nah", "nap", "nb", "ne", "nl", "nn", "no", "nso", "pa", "pap", "pms", "ps", "rm", "sco", "se", "si", "so", "son", "sq", "sv", "sw", "ta", "te", "tk", "ur", "yo", ], PluralForms::None => [ - "ay", "bo", "cgg", "fa", "ht", "id", "ja", "jbo", "ka", "km", "ko", "ky", + "ay", "bo", "cgg", "ht", "id", "ja", "jbo", "ka", "km", "ko", "ky", "lo", "ms", "sah", "su", "th", "tt", "ug", "vi", "wo", "zh", ], PluralForms::Dual_Slavic => [ @@ -90,11 +90,13 @@ module I18next::Plurals "sk" => PluralForms::Special_Czech_Slovak, "sl" => PluralForms::Special_Slovenian, # Mixed v3/v4 rules - "fr" => PluralForms::Special_French_Portuguese, - "hr" => PluralForms::Special_Hungarian_Serbian, - "it" => PluralForms::Special_Spanish_Italian, - "pt-BR" => PluralForms::Special_French_Portuguese, - "sr" => PluralForms::Special_Hungarian_Serbian, + "es" => PluralForms::Special_Spanish_Italian, + "fr" => PluralForms::Special_French_Portuguese, + "hr" => PluralForms::Special_Hungarian_Serbian, + "it" => PluralForms::Special_Spanish_Italian, + "pt" => PluralForms::Special_French_Portuguese, + "pt" => PluralForms::Special_French_Portuguese, + "sr" => PluralForms::Special_Hungarian_Serbian, } # These are the v1 and v2 compatible suffixes. @@ -165,7 +167,7 @@ module I18next::Plurals def get_plural_form(locale : String) : PluralForms # Extract the ISO 639-1 or 639-2 code from an RFC 5646 language code - if !locale.matches?(/^pt-BR$/) + if !locale.matches?(/^pt-PT$/) locale = locale.split('-')[0] end diff --git a/src/invidious/helpers/json_filter.cr b/src/invidious/helpers/json_filter.cr deleted file mode 100644 index 3f4080ba..00000000 --- a/src/invidious/helpers/json_filter.cr +++ /dev/null @@ -1,248 +0,0 @@ -module JSONFilter - alias BracketIndex = Hash(Int64, Int64) - - alias GroupedFieldsValue = String | Array(GroupedFieldsValue) - alias GroupedFieldsList = Array(GroupedFieldsValue) - - class FieldsParser - class ParseError < Exception - end - - # Returns the `Regex` pattern used to match nest groups - def self.nest_group_pattern : Regex - # uses a '.' character to match json keys as they are allowed - # to contain any unicode codepoint - /(?:|,)(?<groupname>[^,\n]*?)\(/ - end - - # Returns the `Regex` pattern used to check if there are any empty nest groups - def self.unnamed_nest_group_pattern : Regex - /^\(|\(\(|\/\(/ - end - - def self.parse_fields(fields_text : String, &) : Nil - if fields_text.empty? - raise FieldsParser::ParseError.new "Fields is empty" - end - - opening_bracket_count = fields_text.count('(') - closing_bracket_count = fields_text.count(')') - - if opening_bracket_count != closing_bracket_count - bracket_type = opening_bracket_count > closing_bracket_count ? "opening" : "closing" - raise FieldsParser::ParseError.new "There are too many #{bracket_type} brackets (#{opening_bracket_count}:#{closing_bracket_count})" - elsif match_result = unnamed_nest_group_pattern.match(fields_text) - raise FieldsParser::ParseError.new "Unnamed nest group at position #{match_result.begin}" - end - - # first, handle top-level single nested properties: items/id, playlistItems/snippet, etc - parse_single_nests(fields_text) { |nest_list| yield nest_list } - - # next, handle nest groups: items(id, etag, etc) - parse_nest_groups(fields_text) { |nest_list| yield nest_list } - end - - def self.parse_single_nests(fields_text : String, &) : Nil - single_nests = remove_nest_groups(fields_text) - - if !single_nests.empty? - property_nests = single_nests.split(',') - - property_nests.each do |nest| - nest_list = nest.split('/') - if nest_list.includes? "" - raise FieldsParser::ParseError.new "Empty key in nest list: #{nest_list}" - end - yield nest_list - end - # else - # raise FieldsParser::ParseError.new "Empty key in nest list 22: #{fields_text} | #{single_nests}" - end - end - - def self.parse_nest_groups(fields_text : String, &) : Nil - nest_stack = [] of NamedTuple(group_name: String, closing_bracket_index: Int64) - bracket_pairs = get_bracket_pairs(fields_text, true) - - text_index = 0 - regex_index = 0 - - while regex_result = self.nest_group_pattern.match(fields_text, regex_index) - raw_match = regex_result[0] - group_name = regex_result["groupname"] - - text_index = regex_result.begin - regex_index = regex_result.end - - if text_index.nil? || regex_index.nil? - raise FieldsParser::ParseError.new "Received invalid index while parsing nest groups: text_index: #{text_index} | regex_index: #{regex_index}" - end - - offset = raw_match.starts_with?(',') ? 1 : 0 - - opening_bracket_index = (text_index + group_name.size) + offset - closing_bracket_index = bracket_pairs[opening_bracket_index] - content_start = opening_bracket_index + 1 - - content = fields_text[content_start...closing_bracket_index] - - if content.empty? - raise FieldsParser::ParseError.new "Empty nest group at position #{content_start}" - else - content = remove_nest_groups(content) - end - - while nest_stack.size > 0 && closing_bracket_index > nest_stack[nest_stack.size - 1][:closing_bracket_index] - if nest_stack.size - nest_stack.pop - end - end - - group_name.split('/').each do |name| - nest_stack.push({ - group_name: name, - closing_bracket_index: closing_bracket_index, - }) - end - - if !content.empty? - properties = content.split(',') - - properties.each do |prop| - nest_list = nest_stack.map { |nest_prop| nest_prop[:group_name] } - - if !prop.empty? - if prop.includes?('/') - parse_single_nests(prop) { |list| nest_list += list } - else - nest_list.push prop - end - else - raise FieldsParser::ParseError.new "Empty key in nest list: #{nest_list << prop}" - end - - yield nest_list - end - end - end - end - - def self.remove_nest_groups(text : String) : String - content_bracket_pairs = get_bracket_pairs(text, false) - - content_bracket_pairs.each_key.to_a.reverse.each do |opening_bracket| - closing_bracket = content_bracket_pairs[opening_bracket] - last_comma = text.rindex(',', opening_bracket) || 0 - - text = text[0...last_comma] + text[closing_bracket + 1...text.size] - end - - return text.starts_with?(',') ? text[1...text.size] : text - end - - def self.get_bracket_pairs(text : String, recursive = true) : BracketIndex - istart = [] of Int64 - bracket_index = BracketIndex.new - - text.each_char_with_index do |char, index| - if char == '(' - istart.push(index.to_i64) - end - - if char == ')' - begin - opening = istart.pop - if recursive || (!recursive && istart.size == 0) - bracket_index[opening] = index.to_i64 - end - rescue - raise FieldsParser::ParseError.new "No matching opening parenthesis at: #{index}" - end - end - end - - if istart.size != 0 - idx = istart.pop - raise FieldsParser::ParseError.new "No matching closing parenthesis at: #{idx}" - end - - return bracket_index - end - end - - class FieldsGrouper - alias SkeletonValue = Hash(String, SkeletonValue) - - def self.create_json_skeleton(fields_text : String) : SkeletonValue - root_hash = {} of String => SkeletonValue - - FieldsParser.parse_fields(fields_text) do |nest_list| - current_item = root_hash - nest_list.each do |key| - if current_item[key]? - current_item = current_item[key] - else - current_item[key] = {} of String => SkeletonValue - current_item = current_item[key] - end - end - end - root_hash - end - - def self.create_grouped_fields_list(json_skeleton : SkeletonValue) : GroupedFieldsList - grouped_fields_list = GroupedFieldsList.new - json_skeleton.each do |key, value| - grouped_fields_list.push key - - nested_keys = create_grouped_fields_list(value) - grouped_fields_list.push nested_keys unless nested_keys.empty? - end - return grouped_fields_list - end - end - - class FilterError < Exception - end - - def self.filter(item : JSON::Any, fields_text : String, in_place : Bool = true) - skeleton = FieldsGrouper.create_json_skeleton(fields_text) - grouped_fields_list = FieldsGrouper.create_grouped_fields_list(skeleton) - filter(item, grouped_fields_list, in_place) - end - - def self.filter(item : JSON::Any, grouped_fields_list : GroupedFieldsList, in_place : Bool = true) : JSON::Any - item = item.clone unless in_place - - if !item.as_h? && !item.as_a? - raise FilterError.new "Can't filter '#{item}' by #{grouped_fields_list}" - end - - top_level_keys = Array(String).new - grouped_fields_list.each do |value| - if value.is_a? String - top_level_keys.push value - elsif value.is_a? Array - if !top_level_keys.empty? - key_to_filter = top_level_keys.last - - if item.as_h? - filter(item[key_to_filter], value, in_place: true) - elsif item.as_a? - item.as_a.each { |arr_item| filter(arr_item[key_to_filter], value, in_place: true) } - end - else - raise FilterError.new "Tried to filter while top level keys list is empty" - end - end - end - - if item.as_h? - item.as_h.select! top_level_keys - elsif item.as_a? - item.as_a.map { |value| filter(value, top_level_keys, in_place: true) } - end - - item - end -end diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index a006d602..e438e3b9 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -262,7 +262,7 @@ def get_referer(env, fallback = "/", unroll = true) end referer = referer.request_target - referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,0-9a-zA-Z]/, "").lstrip("/\\") + referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,*0-9a-zA-Z]/, "").lstrip("/\\") if referer == env.request.path referer = fallback diff --git a/src/invidious/jobs/statistics_refresh_job.cr b/src/invidious/jobs/statistics_refresh_job.cr index 72d1ce88..66c91ad5 100644 --- a/src/invidious/jobs/statistics_refresh_job.cr +++ b/src/invidious/jobs/statistics_refresh_job.cr @@ -56,8 +56,8 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob users = STATISTICS.dig("usage", "users").as(Hash(String, Int64)) users["total"] = Invidious::Database::Statistics.count_users_total - users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_1m - users["activeMonth"] = Invidious::Database::Statistics.count_users_active_6m + users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_6m + users["activeMonth"] = Invidious::Database::Statistics.count_users_active_1m STATISTICS["metadata"] = { "updatedAt" => Time.utc.to_unix, diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index b42ecd1a..12942906 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -191,6 +191,8 @@ module Invidious::Routes::API::V1::Misc json.object do json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]? json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]? + json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]? + json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]? json.field "params", params.try &.as_s json.field "pageType", pageType end diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 9fb283c2..2922b060 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -32,11 +32,14 @@ module Invidious::Routes::API::V1::Search begin client = HTTP::Client.new("suggestqueries-clients6.youtube.com") - url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&xssi=t&gs_ri=youtube&ds=yt" + client.before_request { |r| add_yt_headers(r) } + + url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt" response = client.get(url).body + client.close - body = JSON.parse(response[5..-1]).as_a + body = JSON.parse(response[19..-2]).as_a suggestions = body[1].as_a[0..-2] JSON.build do |json| diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 1017ac9d..9281f4dd 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -363,4 +363,47 @@ module Invidious::Routes::API::V1::Videos end end end + + def self.clips(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + clip_id = env.params.url["id"] + region = env.params.query["region"]? + proxy = {"1", "true"}.any? &.== env.params.query["local"]? + + response = YoutubeAPI.resolve_url("https://www.youtube.com/clip/#{clip_id}") + return error_json(400, "Invalid clip ID") if response["error"]? + + video_id = response.dig?("endpoint", "watchEndpoint", "videoId").try &.as_s + return error_json(400, "Invalid clip ID") if video_id.nil? + + start_time = nil + end_time = nil + clip_title = nil + + if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s + start_time, end_time, clip_title = parse_clip_parameters(params) + end + + begin + video = get_video(video_id, region: region) + rescue ex : NotFoundException + return error_json(404, ex) + rescue ex + return error_json(500, ex) + end + + return JSON.build do |json| + json.object do + json.field "startTime", start_time + json.field "endTime", end_time + json.field "clipTitle", clip_title + json.field "video" do + Invidious::JSONify::APIv1.video(video, json, locale: locale, proxy: proxy) + end + end + end + end end diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 40bca008..e20a7139 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -407,14 +407,23 @@ module Invidious::Routes::Feeds end spawn do - rss = XML.parse_html(body) - rss.xpath_nodes("//feed/entry").each do |entry| - id = entry.xpath_node("videoid").not_nil!.content - author = entry.xpath_node("author/name").not_nil!.content - published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content) - updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content) - - video = get_video(id, force_refresh: true) + # TODO: unify this with the other almost identical looking parts in this and channels.cr somehow? + namespaces = { + "yt" => "http://www.youtube.com/xml/schemas/2015", + "default" => "http://www.w3.org/2005/Atom", + } + rss = XML.parse(body) + rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry| + id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content + author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content + published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content) + updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) + + begin + video = get_video(id, force_refresh: true) + rescue + next # skip this video since it raised an exception (e.g. it is a scheduled live event) + end if CONFIG.enable_user_notifications # Deliver notifications to `/api/v1/auth/notifications` diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 3d935f0a..aabe8dfc 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -275,6 +275,12 @@ module Invidious::Routes::Watch return error_template(400, "Invalid clip ID") if response["error"]? if video_id = response.dig?("endpoint", "watchEndpoint", "videoId") + if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s + start_time, end_time, _ = parse_clip_parameters(params) + env.params.query["start"] = start_time.to_s if start_time != nil + env.params.query["end"] = end_time.to_s if end_time != nil + end + return env.redirect "/watch?v=#{video_id}&#{env.params.query}" else return error_template(404, "The requested clip doesn't exist") diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index d6bd991c..ba05da19 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -235,6 +235,7 @@ module Invidious::Routing get "/api/v1/captions/:id", {{namespace}}::Videos, :captions get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations get "/api/v1/comments/:id", {{namespace}}::Videos, :comments + get "/api/v1/clips/:id", {{namespace}}::Videos, :clips # Feeds get "/api/v1/trending", {{namespace}}::Feeds, :trending diff --git a/src/invidious/search/filters.cr b/src/invidious/search/filters.cr index c2b5c758..bf968734 100644 --- a/src/invidious/search/filters.cr +++ b/src/invidious/search/filters.cr @@ -300,9 +300,9 @@ module Invidious::Search object["9:varint"] = ((page - 1) * 20).to_i64 end - # If the object is empty, return an empty string, - # otherwise encode to protobuf then to base64 - return "" if object.empty? + # Prevent censoring of self harm topics + # See https://github.com/iv-org/invidious/issues/4398 + object["30:varint"] = 1.to_i64 return object .try { |i| Protodec::Any.cast_json(i) } diff --git a/src/invidious/videos/clip.cr b/src/invidious/videos/clip.cr new file mode 100644 index 00000000..29c57182 --- /dev/null +++ b/src/invidious/videos/clip.cr @@ -0,0 +1,22 @@ +require "json" + +# returns start_time, end_time and clip_title +def parse_clip_parameters(params) : {Float64?, Float64?, String?} + decoded_protobuf = params.try { |i| URI.decode_www_form(i) } + .try { |i| Base64.decode(i) } + .try { |i| IO::Memory.new(i) } + .try { |i| Protodec::Any.parse(i) } + + start_time = decoded_protobuf + .try(&.["50:0:embedded"]["2:1:varint"].as_i64) + .try { |i| i/1000 } + + end_time = decoded_protobuf + .try(&.["50:0:embedded"]["3:2:varint"].as_i64) + .try { |i| i/1000 } + + clip_title = decoded_protobuf + .try(&.["50:0:embedded"]["4:3:string"].as_s) + + return start_time, end_time, clip_title +end diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 031b46da..6d227cfc 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -82,11 +82,19 @@ </div> <div class="video-card-row flexible"> - <div class="flex-left"><a href="/channel/<%= item.ucid %>"> - <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> - <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> - </p> - </a></div> + <div class="flex-left"> + <% if !item.ucid.to_s.empty? %> + <a href="/channel/<%= item.ucid %>"> + <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> + <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> + </p> + </a> + <% else %> + <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> + <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> + </p> + <% end %> + </div> </div> <% when Category %> <% else %> @@ -160,11 +168,19 @@ </div> <div class="video-card-row flexible"> - <div class="flex-left"><a href="/channel/<%= item.ucid %>"> - <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> - <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> - </p> - </a></div> + <div class="flex-left"> + <% if !item.ucid.to_s.empty? %> + <a href="/channel/<%= item.ucid %>"> + <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> + <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> + </p> + </a> + <% else %> + <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %> + <%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%> + </p> + <% end %> + </div> <%= rendered "components/video-context-buttons" %> </div> diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 5e2cf88e..5623bf77 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -1,5 +1,9 @@ +<% + locale = env.get("preferences").as(Preferences).locale + dark_mode = env.get("preferences").as(Preferences).dark_mode +%> <!DOCTYPE html> -<html lang="<%= env.get("preferences").as(Preferences).locale %>"> +<html lang="<%= locale %>"> <head> <meta charset="utf-8"> @@ -20,13 +24,8 @@ <script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script> </head> -<% - locale = env.get("preferences").as(Preferences).locale - dark_mode = env.get("preferences").as(Preferences).dark_mode -%> - <body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme"> - <span style="display:none" id="dark_mode_pref"><%= env.get("preferences").as(Preferences).dark_mode %></span> + <span style="display:none" id="dark_mode_pref"><%= dark_mode %></span> <div class="pure-g"> <div class="pure-u-1 pure-u-xl-20-24" id="contents"> <div class="pure-g navbar h-box"> @@ -42,8 +41,8 @@ <div class="pure-u-1 pure-u-md-8-24 user-field"> <% if env.get? "user" %> <div class="pure-u-1-4"> - <a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode == "dark" %> + <a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>"> + <% if dark_mode == "dark" %> <i class="icon ion-ios-sunny"></i> <% else %> <i class="icon ion-ios-moon"></i> @@ -80,8 +79,8 @@ </div> <% else %> <div class="pure-u-1-3"> - <a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode == "dark" %> + <a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>"> + <% if dark_mode == "dark" %> <i class="icon ion-ios-sunny"></i> <% else %> <i class="icon ion-ios-moon"></i> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 07474896..7a1cf2c3 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -118,7 +118,7 @@ we're going to need to do it here in order to allow for translations. link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}") if !plid.nil? && !continuation.nil? - link_yt_param = URI::Params{"plid" => [plid], "index" => [continuation.to_s]} + link_yt_param = URI::Params{"list" => [plid], "index" => [continuation.to_s]} link_yt_watch = IV::HttpServer::Utils.add_params_to_url(link_yt_watch, link_yt_param) link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param) end @@ -346,7 +346,7 @@ we're going to need to do it here in order to allow for translations. <h5 class="pure-g"> <div class="pure-u-14-24"> - <% if rv["ucid"]? %> + <% if !rv["ucid"].empty? %> <b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b> <% else %> <b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></b> diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 56325cf7..0e72957e 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -822,9 +822,9 @@ module HelperExtractors end # Retrieves the ID required for querying the InnerTube browse endpoint. - # Raises when it's unable to do so + # Returns an empty string when it's unable to do so def self.get_browse_id(container) - return container.dig("navigationEndpoint", "browseEndpoint", "browseId").as_s + return container.dig?("navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || "" end end |
