Merge branch 'master' into add_missing_locale

This commit is contained in:
syeopite 2021-10-03 19:12:22 +00:00 committed by GitHub
commit 38075944e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
152 changed files with 66959 additions and 6740 deletions

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
# https://github.community/t/how-to-change-the-category/2261/3
videojs-*.js linguist-detectable=false
video.min.js linguist-detectable=false

15
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,15 @@
# Default and lowest precedence. If none of the below matches, @iv-org/developers would be requested for review.
* @iv-org/developers
docker-compose.yml @unixfox
docker/ @unixfox
kubernetes/ @unixfox
README.md @thefrenchghosty
config/config.example.yml @thefrenchghosty @SamantazFox @unixfox
scripts/ @syeopite
shards.lock @syeopite
shards.yml @syeopite
src/invidious/helpers/youtube_api.cr @SamantazFox

View File

@ -1,41 +1,72 @@
name: Invidious CI
on:
schedule:
- cron: "0 0 * * *" # Every day at 00:00
push:
branches:
- "master"
- "api-only"
pull_request:
branches: "*"
paths-ignore:
- "*.md"
- LICENCE
- TRANSLATION
- invidious.service
- .git*
- .editorconfig
- screenshots/*
- assets/**
- locales/*
- config/**
- .github/ISSUE_TEMPLATE/*
- kubernetes/**
jobs:
build:
runs-on: ubuntu-latest
name: "build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }}"
continue-on-error: ${{ !matrix.stable }}
strategy:
fail-fast: false
matrix:
stable: [true]
crystal:
- 1.0.0
- 1.1.1
include:
- crystal: nightly
stable: false
steps:
- uses: actions/checkout@v2
- name: Install Crystal
uses: oprypin/install-crystal@v1.2.4
with:
crystal: 0.36.1
crystal: ${{ matrix.crystal }}
- name: Cache Shards
uses: actions/cache@v2
with:
path: ./lib
key: shards-${{ hashFiles('shard.lock') }}
- name: Install Shards
run: |
if ! shards check; then
shards install
fi
- name: Run tests
run: crystal spec
- name: Run lint
run: |
if ! crystal tool format --check; then
@ -43,20 +74,50 @@ jobs:
git diff
exit 1
fi
- name: Build
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
build-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build Docker
run: docker-compose build --build-arg release=0
- name: Run Docker
run: docker-compose up -d
- name: Test Docker
run: while curl -Isf http://localhost:3000; do sleep 1; done
build-docker-arm64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build Docker ARM64 image
uses: docker/build-push-action@v2
with:
context: .
file: docker/Dockerfile.arm64
platforms: linux/arm64/v8
build-args: release=0
- name: Test Docker
run: while curl -Isf http://localhost:3000; do sleep 1; done

View File

@ -4,9 +4,18 @@ on:
push:
branches:
- "master"
schedule:
- cron: 0 0 * * *
paths-ignore:
- "*.md"
- LICENCE
- TRANSLATION
- invidious.service
- .git*
- .editorconfig
- screenshots/*
- .github/ISSUE_TEMPLATE/*
- kubernetes/**
jobs:
release:
runs-on: ubuntu-latest
@ -14,9 +23,24 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Crystal
uses: oprypin/install-crystal@v1.2.4
with:
crystal: 1.1.1
- name: Run lint
run: |
if ! crystal tool format --check; then
crystal tool format
git diff
exit 1
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
@ -28,12 +52,26 @@ jobs:
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- name: Build and push for Push Event
- name: Build and push Docker AMD64 image for Push Event
if: github.ref == 'refs/heads/master'
uses: docker/build-push-action@v2
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64
labels: quay.expires-after=12w
push: true
tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest
build-args: release=1
- name: Build and push Docker ARM64 image for Push Event
if: github.ref == 'refs/heads/master'
uses: docker/build-push-action@v2
with:
context: .
file: docker/Dockerfile.arm64
platforms: linux/arm64/v8
labels: quay.expires-after=12w
push: true
tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64
build-args: release=1

22
.github/workflows/lock.yml vendored Normal file
View File

@ -0,0 +1,22 @@
# Documentation: https://github.com/marketplace/actions/lock-threads
name: 'Lock Threads'
on:
workflow_dispatch:
schedule:
- cron: "0 */12 * * *"
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '240'
pr-lock-inactive-days: '240'
issue-lock-reason: 'resolved'
pr-lock-reason: 'resolved'
# issue-lock-comment: 'This issue has been automatically locked since there has not been any activity in it in the last 30 days. If this is still applicable to the current version of Invidious feel free to open a new issue.'
# pr-lock-comment: 'This pull request has been automatically locked since there has not been any activity in it in the last 30 days. If you want to tell us about needed or wanted changes or if problems related to this code are discovered, feel free to open an issue or a new pull request.'

View File

@ -14,9 +14,11 @@ jobs:
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 365
days-before-pr-stale: 45 # PRs should be active. Anything that hasn't had activity in more than 45 days should be considered abandoned.
days-before-close: 30
exempt-pr-labels: blocked
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.'
stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.'
stale-issue-label: "stale"
stale-pr-label: "stale"
ascending: true

180
README.md
View File

@ -1,73 +1,133 @@
<h1 align="center">Invidious</h1>
<div align="center">
<img src="assets/invidious-colored-vector.svg" width="192" height="192" alt="Invidious logo">
<h1>Invidious</h1>
<h2 align="center">Invidious is an alternative front-end to YouTube.</h2>
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html">
<img alt="License: AGPLv3+" src="https://shields.io/badge/License-AGPL%20v3+-blue.svg">
</a>
<a href="https://github.com/iv-org/invidious/actions">
<img alt="Build Status" src="https://github.com/iv-org/invidious/workflows/Invidious%20CI/badge.svg">
</a>
<a href="https://github.com/iv-org/invidious/issues">
<img alt="GitHub issues" src="https://img.shields.io/github/issues/iv-org/invidious?color=important">
</a>
<a href="https://github.com/iv-org/invidious/pulls">
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/iv-org/invidious?color=blueviolet">
</a>
<a href="https://hosted.weblate.org/engage/invidious/">
<img alt="Translation Status" src="https://hosted.weblate.org/widgets/invidious/-/translations/svg-badge.svg">
</a>
<a href="https://github.com/humanetech-community/awesome-humane-tech">
<img alt="Awesome Humane Tech" src="https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true">
</a>
---
<h3>An open source alternative front-end to YouTube</h3>
## Invidious instances:
<a href="https://instances.invidious.io/">Instances list</a>
&nbsp;&nbsp;
<a href="#documentation">Documentation</a>
&nbsp;&nbsp;
<a href="#contribute">Contribute</a>
&nbsp;&nbsp;
<a href="#donate">Donate</a>
<h5>Chat with us:</h5>
<a href="https://matrix.to/#/#invidious:matrix.org">
<img alt="Matrix" src="https://img.shields.io/matrix/invidious:matrix.org?label=Matrix&color=darkgreen">
</a>
<a href="https://web.libera.chat/?channel=#invidious">
<img alt="Libera.chat (IRC)" src="https://img.shields.io/badge/IRC%20%28Libera.chat%29-%23invidious-darkgreen">
</a>
</div>
Public Invidious instances are listed on the documentation website: https://instances.invidious.io/
---
## Screenshots
## Invidious features:
| Player | Preferences | Subscriptions |
|-------------------------------------|-------------------------------------|---------------------------------------|
| ![](screenshots/01_player.png) | ![](screenshots/02_preferences.png) | ![](screenshots/03_subscriptions.png) |
| ![](screenshots/04_description.png) | ![](screenshots/05_preferences.png) | ![](screenshots/06_subscriptions.png) |
- [Copylefted libre software](https://github.com/iv-org/invidious) (AGPLv3+ licensed)
- Lightweight (the homepage is ~4 KB compressed)
## Features
**User features**
- Lightweight
- No ads
- No tracking
- Javascript is 100% optional
- Tools for managing subscriptions:
- Only show unseen videos
- Only show latest (or latest unseen) video from each channel
- Delivers notifications from all subscribed channels
- Automatically redirect homepage to feed
- Import subscriptions from YouTube
- Audio-only mode (and no need to keep window open on mobile)
- Dark mode
- Embed support
- Set default player options (speed, quality, autoplay, loop)
- Support for Reddit comments in place of YouTube comments
- Import/Export subscriptions, watch history, preferences
- No JavaScript required
- Light/Dark themes
- Customizable homepage
- Subscriptions independant from Google
- Notifications for all subscribed channels
- Audio-only mode (with background play on mobile)
- Support for Reddit comments
- [Available in many languages](locales/), thanks to [our translators](#contribute)
**Data import/export**
- Import subscriptions from YouTube, NewPipe and Freetube
- Import watch history from NewPipe
- Export subscriptions to NewPipe and Freetube
- Import/Export Invidious user data
**Technical features**
- Embedded video support
- [Developer API](https://docs.invidious.io/API.md)
- Does not use any of the official YouTube APIs
- No need to create a Google account to save subscriptions
- No Code of Conduct
- No Contributor license Agreement
- Available in many languages, thanks to [Weblate](https://hosted.weblate.org/projects/invidious/)
- Does not use official YouTube APIs
- No Contributor License Agreement (CLA)
---
## Screenshots:
## Quick start
| Player | Preferences | Subscriptions |
| ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [<img src="screenshots/01_player.png?raw=true" height="140" width="280">](screenshots/01_player.png?raw=true) | [<img src="screenshots/02_preferences.png?raw=true" height="140" width="280">](screenshots/02_preferences.png?raw=true) | [<img src="screenshots/03_subscriptions.png?raw=true" height="140" width="280">](screenshots/03_subscriptions.png?raw=true) |
| [<img src="screenshots/04_description.png?raw=true" height="140" width="280">](screenshots/04_description.png?raw=true) | [<img src="screenshots/05_preferences.png?raw=true" height="140" width="280">](screenshots/05_preferences.png?raw=true) | [<img src="screenshots/06_subscriptions.png?raw=true" height="140" width="280">](screenshots/06_subscriptions.png?raw=true) |
**Using invidious:**
---
- [Select a public instance from the list](https://instances.invidious.io) and start watching videos right now!
## Donate:
**Hosting invidious:**
Bitcoin (BTC): [bc1qfhe7rq3lqzuayzjxzyt9waz9ytrs09kla3tsgr](bitcoin:bc1qfhe7rq3lqzuayzjxzyt9waz9ytrs09kla3tsgr)
- [Follow the installation instructions](https://docs.invidious.io/Installation.md)
Monero (XMR): [41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3LXqkEJBrCZBgPTwJrDp1FrZJfycGPR](monero:41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3LXqkEJBrCZBgPTwJrDp1FrZJfycGPR)
---
## Documentation
## Documentation:
The full documentation can be accessed online at https://docs.invidious.io/
The complete documentation is available on https://docs.invidious.io/ (or alternatively on its own [Github repository](https://github.com/iv-org/documentation)).
The documentation's source code is available in this repository:
https://github.com/iv-org/documentation
---
### Extensions
## Extensions:
We highly recommend the use of [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect#get),
a browser extension that automatically redirects Youtube URLs to any Invidious instance and replaces
embedded youtube videos on other websites with invidious.
[Extensions](https://docs.invidious.io/Extensions.md) can be found in the wiki, as well as documentation for integrating it into other projects.
The documentation contains a list of browser extensions that we recommended to use along with Invidious.
---
You can read more here: https://docs.invidious.io/Extensions.md
## Made with Invidious:
## Contribute
### Code
1. Fork it ( https://github.com/iv-org/invidious/fork ).
1. Create your feature branch (`git checkout -b my-new-feature`).
1. Stage your files (`git add .`).
1. Commit your changes (`git commit -am 'Add some feature'`).
1. Push to the branch (`git push origin my-new-feature`).
1. Create a new pull request ( https://github.com/iv-org/invidious/compare ).
### Translations
We use [Weblate](https://weblate.org) to manage Invidious translations.
You can suggest new translations and/or correction here: https://hosted.weblate.org/engage/invidious/.
Creating an account is not required, but recommended, especially if you want to contribute regularly.
Weblate also allows you to log-in with major SSO providers like Github, Gitlab, BitBucket, Google, ...
## Projects using Invidious
- [FreeTube](https://github.com/FreeTubeApp/FreeTube): A libre software YouTube app for privacy.
- [CloudTube](https://sr.ht/~cadence/tube/): A JavaScript-rich alternate YouTube player.
@ -75,33 +135,25 @@ The complete documentation is available on https://docs.invidious.io/ (or altern
- [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A material design music player that streams music from YouTube.
- [HoloPlay](https://github.com/stephane-r/HoloPlay): Funny Android application connecting on Invidious API's with search, playlists and favoris.
---
## Contributing:
## Donate
[![Build Status](https://github.com/iv-org/invidious/workflows/Invidious%20CI/badge.svg)](https://github.com/iv-org/invidious/actions) [![Translation Status](https://hosted.weblate.org/widgets/invidious/-/translations/svg-badge.svg)](https://hosted.weblate.org/engage/invidious/)
Bitcoin (BTC): [bc1qfhe7rq3lqzuayzjxzyt9waz9ytrs09kla3tsgr](bitcoin:bc1qfhe7rq3lqzuayzjxzyt9waz9ytrs09kla3tsgr)
1. Fork it ( https://github.com/iv-org/invidious/fork ).
2. Create your feature branch (git checkout -b my-new-feature).
3. Commit your changes (git commit -am 'Add some feature').
4. Push to the branch (git push origin my-new-feature).
5. Create a new pull request.
Monero (XMR): [41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3LXqkEJBrCZBgPTwJrDp1FrZJfycGPR](monero:41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3LXqkEJBrCZBgPTwJrDp1FrZJfycGPR)
### Translation:
Ethereum (ETH): [0xD1F7E3Bfb19Ee5a52baED396Ad34717aF18d995B](ethereum:0xD1F7E3Bfb19Ee5a52baED396Ad34717aF18d995B)
- Log in with an account you have elsewhere, or register an account and start translating at [Hosted Weblate](https://hosted.weblate.org/engage/invidious/).
Litecoin (LTC): [ltc1q8787aq2xrseq5yx52axx8c4fqks88zj5vr0zx9](litecoin:ltc1q8787aq2xrseq5yx52axx8c4fqks88zj5vr0zx9)
---
## Contact:
## Liability
Feel free to join our [Matrix room](https://matrix.to/#/#invidious:matrix.org), or #invidious on freenode. Both platforms are bridged together.
---
## Liability:
We take no responsibility for the use of our tool, or external instances provided by third parties. We strongly recommend you abide by the valid official regulations in your country. Furthermore, we refuse liability for any inappropriate use of Invidious, such as illegal downloading. This tool is provided to you in the spirit of free, open software.
We take no responsibility for the use of our tool, or external instances
provided by third parties. We strongly recommend you abide by the valid
official regulations in your country. Furthermore, we refuse liability
for any inappropriate use of Invidious, such as illegal downloading.
This tool is provided to you in the spirit of free, open software.
You may view the LICENSE in which this software is provided to you [here](./LICENSE).

View File

@ -5,6 +5,12 @@ body {
Arial, sans-serif;
}
#contents {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.deleted {
background-color: rgb(255, 0, 0, 0.5);
}
@ -172,7 +178,7 @@ img.thumbnail {
flex: 1;
}
.navbar > .searchbar {
.searchbar {
flex-grow: 2; /* take double the space of the other items */
}
@ -185,7 +191,7 @@ img.thumbnail {
display: inline;
}
.navbar > .searchbar .pure-form input[type="search"] {
.searchbar .pure-form input[type="search"] {
margin-bottom: 1px;
border-top: 0;
@ -210,12 +216,12 @@ input[type="search"]::-webkit-search-cancel-button {
background-size: 14px;
}
.navbar > .searchbar .pure-form fieldset {
.searchbar .pure-form fieldset {
padding: 0;
}
/* attract focus to the searchbar by adding a subtle transition */
.navbar > .searchbar .pure-form input[type="search"]:focus {
.searchbar .pure-form input[type="search"]:focus {
margin-bottom: 0px;
border-bottom: 2px solid #aaa;
}
@ -276,18 +282,35 @@ input[type="search"]::-webkit-search-cancel-button {
}
}
/*
* Video "cards" (results/playlist/channel videos)
*/
.video-card-row { margin: 15px 0; }
.flexible { display: flex; }
.flex-left { flex: 1 1 100%; flex-wrap: wrap; }
.flex-right { flex: 1 0 max-content; flex-wrap: nowrap; }
p.channel-name { margin: 0; }
p.video-data { margin: 0; font-weight: bold; font-size: 80%; }
/*
* Footer
*/
.footer {
color: #666666;
margin: 2em 0;
footer {
color: #919191;
margin-top: auto;
padding: 1.5em 0;
text-align: center;
max-height: 30vh;
}
body .footer a {
color: inherit;
footer a {
color: #919191 !important;
text-decoration: underline;
}
@ -302,194 +325,15 @@ body .footer a {
}
}
/* Control Bar */
@media screen and (max-width: 640px) {
.video-js .vjs-control-bar,
.vjs-menu-button-popup .vjs-menu .vjs-menu-content {
overflow-x: scroll;
}
}
ul.vjs-menu-content::-webkit-scrollbar {
display: none;
}
.vjs-user-inactive {
cursor: none;
}
.video-js .vjs-text-track-display > div > div > div {
background-color: rgba(0, 0, 0, 0.75) !important;
border-radius: 9px !important;
padding: 5px !important;
}
.vjs-play-control,
.vjs-volume-panel,
.vjs-current-time,
.vjs-time-control,
.vjs-duration,
.vjs-progress-control,
.vjs-remaining-time {
order: 1;
}
.vjs-captions-button {
order: 2;
}
.vjs-quality-selector,
.video-js .vjs-http-source-selector {
order: 3;
}
.vjs-playback-rate {
order: 4;
}
.vjs-share-control {
order: 5;
}
.vjs-fullscreen-control {
order: 6;
}
.vjs-playback-rate > .vjs-menu {
width: 50px;
}
.vjs-control-bar {
display: flex;
flex-direction: row;
scrollbar-width: none;
}
.vjs-control-bar::-webkit-scrollbar {
display: none;
}
.video-js .vjs-icon-cog {
font-size: 18px;
}
.video-js .vjs-control-bar,
.vjs-menu-button-popup .vjs-menu .vjs-menu-content {
background-color: rgba(35, 35, 35, 0.75);
}
.vjs-menu li.vjs-menu-item:focus,
.vjs-menu li.vjs-menu-item:hover {
background-color: rgba(255, 255, 255, 0.75);
color: rgba(49, 49, 51, 0.75);
}
.vjs-menu li.vjs-selected,
.vjs-menu li.vjs-selected:focus,
.vjs-menu li.vjs-selected:hover {
background-color: rgba(0, 182, 240, 0.75);
}
/* Progress Bar */
.video-js .vjs-slider {
background-color: rgba(15, 15, 15, 0.5);
}
fieldset > select,
span > select {
color: rgba(49, 49, 51, 1);
}
.video-js .vjs-load-progress,
.video-js .vjs-load-progress div {
background: rgba(87, 87, 88, 1);
}
.video-js .vjs-slider:hover,
.video-js button:hover {
color: rgba(0, 182, 240, 1);
}
.video-js .vjs-play-progress {
background-color: rgba(0, 182, 240, 1);
}
/* Overlay */
.video-js .vjs-overlay {
background-color: rgba(35, 35, 35, 0.75);
color: rgba(255, 255, 255, 1);
}
/* ProgressBar marker */
.vjs-marker {
background-color: rgba(255, 255, 255, 1);
z-index: 0;
}
/* Big "Play" Button */
.video-js .vjs-big-play-button {
background-color: rgba(35, 35, 35, 0.5);
}
.video-js:hover .vjs-big-play-button {
background-color: rgba(35, 35, 35, 0.75);
}
.video-js .vjs-current-time,
.video-js .vjs-time-divider,
.video-js .vjs-duration {
display: block;
}
.video-js .vjs-time-divider {
min-width: 0px;
padding-left: 0px;
padding-right: 0px;
}
.video-js .vjs-poster {
background-size: cover;
object-fit: cover;
}
.player-dimensions.vjs-fluid {
padding-top: 82vh;
}
video.video-js {
position: absolute;
height: 100%;
}
#player-container {
position: relative;
padding-bottom: 82vh;
height: 0;
}
.pure-control-group label {
word-wrap: normal;
}
.video-js.player-style-invidious {
/* This is already the default */
}
.video-js.player-style-youtube .vjs-control-bar {
display: flex;
flex-direction: row;
}
.video-js.player-style-youtube .vjs-big-play-button {
/*
Styles copied from video-js.min.css, definition of
.vjs-big-play-centered .vjs-big-play-button
*/
top: 50%;
left: 50%;
margin-top: -0.81666em;
margin-left: -1.5em;
}
/*
* Light theme
*/
@ -586,7 +430,7 @@ body.dark-theme {
color: #f0f0f0;
}
.dark-theme .navbar > .searchbar input {
.dark-theme .searchbar input {
background-color: inherit;
color: inherit;
}
@ -625,7 +469,7 @@ body.dark-theme {
color: #f0f0f0;
}
.no-theme .navbar > .searchbar input {
.no-theme .searchbar input {
background-color: inherit;
color: inherit;
}
@ -641,7 +485,7 @@ body.dark-theme {
}
#filters > summary {
display: inline-block;
display: block;
margin-bottom: 15px;
}
@ -654,3 +498,50 @@ body.dark-theme {
content: "[ - ]";
font-size: 1.5em;
}
/*With commit d9528f5 all contents of the page is now within a flexbox. However,
the hr element is rendered improperly within one.
See https://stackoverflow.com/a/34372979 for more info */
hr {
margin: 10px 0 10px 0;
}
/* Description Expansion Styling*/
#descexpansionbutton {
display: none
}
#descexpansionbutton ~ div {
overflow: hidden;
height: 8.3em;
}
#descexpansionbutton:checked ~ div {
overflow: unset;
height: 100%;
}
#descexpansionbutton ~ label {
order: 1;
margin-top: 20px;
}
/* Bidi (bidirectional text) support */
h1,
h2,
h3,
h4,
h5,
p,
#descriptionWrapper,
#description-box {
unicode-bidi: plaintext;
text-align: start;
}
#descriptionWrapper {
max-width: 600px;
}
/* Center the "invidious" logo on the search page */
#logo > h1 { text-align: center; }

View File

@ -8,3 +8,19 @@
height: auto;
z-index: -100;
}
.watch-on-invidious {
font-size: 1.3em !important;
font-weight: bold;
white-space: nowrap;
margin: 0 1em 0 1em !important;
order: 3;
}
.watch-on-invidious > a {
color: white;
}
.watch-on-invidious > a:hover {
color: rgba(0, 182, 240, 1);;
}

16
assets/css/empty.css Normal file
View File

@ -0,0 +1,16 @@
#search-widget {
text-align: center;
margin: 20vh 0 50px 0;
}
#logo > h1 {
font-size: 3.5em;
margin: 0;
padding: 0;
}
@media screen and (max-width: 1500px) and (max-height: 1000px) {
#logo > h1 {
font-size: 10vmin;
}
}

250
assets/css/player.css Normal file
View File

@ -0,0 +1,250 @@
/* Youtube player style */
.video-js.player-style-youtube .vjs-progress-control {
height: 0;
}
.video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, .video-js.player-style-youtube .vjs-progress-control {
position: absolute;
right: 0;
left: 0;
width: 100%;
margin: 0;
}
.video-js.player-style-youtube .vjs-control-bar {
background: linear-gradient(rgba(0,0,0,0.1), rgba(0, 0, 0,0.5));
}
.video-js.player-style-youtube .vjs-slider {
background-color: rgba(255,255,255,0.2);
}
.video-js.player-style-youtube .vjs-load-progress > div {
background-color: rgba(255,255,255,0.5);
}
.video-js.player-style-youtube .vjs-play-progress {
background-color: red;
}
.video-js.player-style-youtube .vjs-progress-control:hover .vjs-progress-holder {
font-size: 15px;
}
.video-js.player-style-youtube .vjs-control-bar > .vjs-spacer {
flex: 1;
order: 2;
}
.video-js.player-style-youtube .vjs-play-progress .vjs-time-tooltip {
display: none;
}
.video-js.player-style-youtube .vjs-play-progress::before {
color: red;
font-size: 0.85em;
display: none;
}
.video-js.player-style-youtube .vjs-progress-holder:hover .vjs-play-progress::before {
display: unset;
}
.video-js.player-style-youtube .vjs-control-bar {
display: flex;
flex-direction: row;
}
.video-js.player-style-youtube .vjs-big-play-button {
/*
Styles copied from video-js.min.css, definition of
.vjs-big-play-centered .vjs-big-play-button
*/
top: 50%;
left: 50%;
margin-top: -0.81666em;
margin-left: -1.5em;
}
.video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu {
margin-bottom: 2em;
}
ul.vjs-menu-content::-webkit-scrollbar {
display: none;
}
.vjs-user-inactive {
cursor: none;
}
.video-js .vjs-text-track-display > div > div > div {
background-color: rgba(0, 0, 0, 0.75) !important;
border-radius: 9px !important;
padding: 5px !important;
}
.vjs-play-control,
.vjs-volume-panel,
.vjs-current-time,
.vjs-time-control,
.vjs-duration,
.vjs-progress-control,
.vjs-remaining-time {
order: 1;
}
.vjs-captions-button {
order: 2;
}
.vjs-quality-selector,
.video-js .vjs-http-source-selector {
order: 3;
}
.vjs-playback-rate {
order: 4;
}
.vjs-share-control {
order: 5;
}
.vjs-fullscreen-control {
order: 6;
}
.vjs-playback-rate > .vjs-menu {
width: 50px;
}
.vjs-control-bar {
display: flex;
flex-direction: row;
scrollbar-width: none;
}
.vjs-control-bar::-webkit-scrollbar {
display: none;
}
.video-js .vjs-icon-cog {
font-size: 18px;
}
.video-js .vjs-control-bar,
.vjs-menu-button-popup .vjs-menu .vjs-menu-content {
background-color: rgba(35, 35, 35, 0.75);
}
.vjs-menu li.vjs-menu-item:focus,
.vjs-menu li.vjs-menu-item:hover {
background-color: rgba(255, 255, 255, 0.75);
color: rgba(49, 49, 51, 0.75);
}
.vjs-menu li.vjs-selected,
.vjs-menu li.vjs-selected:focus,
.vjs-menu li.vjs-selected:hover {
background-color: rgba(0, 182, 240, 0.75);
}
/* Progress Bar */
.video-js .vjs-slider {
background-color: rgba(15, 15, 15, 0.5);
}
.video-js .vjs-load-progress,
.video-js .vjs-load-progress div {
background: rgba(87, 87, 88, 1);
}
.video-js .vjs-slider:hover,
.video-js button:hover {
color: rgba(0, 182, 240, 1);
}
.video-js.player-style-invidious .vjs-play-progress {
background-color: rgba(0, 182, 240, 1);
}
vjs-menu-content
/* Overlay */
.video-js .vjs-overlay {
background-color: rgba(35, 35, 35, 0.75);
color: rgba(255, 255, 255, 1);
}
/* ProgressBar marker */
.vjs-marker {
background-color: rgba(255, 255, 255, 1);
z-index: 0;
}
/* Big "Play" Button */
.video-js .vjs-big-play-button {
background-color: rgba(35, 35, 35, 0.5);
}
.video-js:hover .vjs-big-play-button {
background-color: rgba(35, 35, 35, 0.75);
}
.video-js .vjs-current-time,
.video-js .vjs-time-divider,
.video-js .vjs-duration {
display: block;
}
.video-js .vjs-time-divider {
min-width: 0px;
padding-left: 0px;
padding-right: 0px;
}
.video-js .vjs-poster {
background-size: cover;
object-fit: cover;
}
.player-dimensions.vjs-fluid {
padding-top: 82vh;
}
video.video-js {
position: absolute;
height: 100%;
}
#player-container {
position: relative;
padding-bottom: 82vh;
height: 0;
}
.mobile-operations-bar {
display: flex;
position: absolute;
top: 0;
right: 1px !important;
left: initial !important;
width: initial !important;
}
.mobile-operations-bar ul {
position: absolute !important;
bottom: unset !important;
top: 1.5em;
}
@media screen and (max-width: 700px) {
.video-js .vjs-share {
justify-content: unset;
}
}
@media screen and (max-width: 650px) {
.vjs-modal-dialog-content {
overflow-x: hidden;
}
}

View File

@ -0,0 +1,7 @@
/**
* videojs-mobile-ui
* @version 0.5.2
* @copyright 2021 mister-ben <git@misterben.me>
* @license MIT
*/
@keyframes fadeAndScale{0%{opacity:0}25%{opacity:1}100%{opacity:0}}.video-js.vjs-has-started .vjs-touch-overlay{position:absolute;pointer-events:auto;top:0}.video-js .vjs-touch-overlay{display:block;width:100%;height:100%;pointer-events:none}.video-js .vjs-touch-overlay.skip{opacity:0;animation:fadeAndScale 0.6s linear;background-repeat:no-repeat;background-position:80% center;background-size:10%;background-image:url('data:image/svg+xml;utf8,<svg fill="%23FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>')}.video-js .vjs-touch-overlay.skip.reverse{background-position:20% center;background-image:url('data:image/svg+xml;utf8,<svg fill="%23FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>')}.video-js .vjs-touch-overlay .vjs-play-control{top:50%;left:50%;transform:translate(-50%, -50%);position:absolute;width:30%;height:80%;pointer-events:none;opacity:0;transition:opacity 0.3s ease}.video-js .vjs-touch-overlay .vjs-play-control .vjs-icon-placeholder::before{content:'';background-size:60%;background-position:center center;background-repeat:no-repeat;background-image:url('data:image/svg+xml;utf8,<svg fill="%23FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>')}.video-js .vjs-touch-overlay .vjs-play-control.vjs-paused .vjs-icon-placeholder::before{content:'';background-image:url('data:image/svg+xml;utf8,<svg fill="%23FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M8 5v14l11-7z"/><path d="M0 0h24v24H0z" fill="none"/></svg>')}.video-js .vjs-touch-overlay .vjs-play-control.vjs-ended .vjs-icon-placeholder::before{content:'';background-image:url('data:image/svg+xml;utf8,<svg fill="%23FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>')}.video-js .vjs-touch-overlay.show-play-toggle .vjs-play-control{opacity:1;pointer-events:auto}.video-js.vjs-mobile-ui-disable-end.vjs-ended .vjs-touch-overlay{display:none}

View File

@ -0,0 +1 @@
.video-js .vjs-big-vr-play-button{width:100px;height:100px;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='360' height='360' viewBox='0 0 360 360'%3E%3Cpath fill='%23FFF' d='M334.883 275.78l-6.374-36.198-6.375-36.2-28.16 23.62-28.164 23.62 25.837 9.41C266.247 296.544 224 320.5 176.25 320.5c-77.47 0-140.5-63.03-140.5-140.5 0-77.472 63.03-140.5 140.5-140.5 53.428 0 99.98 29.978 123.733 73.993l13.304-6.923C287.025 57.76 235.45 24.5 176.25 24.5c-85.743 0-155.5 69.757-155.5 155.5 0 85.742 69.757 155.5 155.5 155.5 54.253 0 102.09-27.94 129.922-70.177l28.71 10.457z'/%3E%3Cpath fill='%23FFF' d='M314.492 175.167c-12.98 0-23.54-10.56-23.54-23.54s10.56-23.54 23.54-23.54c12.98 0 23.54 10.56 23.54 23.54s-10.56 23.54-23.54 23.54zm0-38.08c-8.018 0-14.54 6.522-14.54 14.54s6.522 14.54 14.54 14.54c8.017 0 14.54-6.522 14.54-14.54s-6.523-14.54-14.54-14.54z'/%3E%3Cg fill='%23FFF'%3E%3Cpath d='M88.76 173.102h9.395c4.74-.042 8.495-1.27 11.268-3.682 2.77-2.412 4.157-5.903 4.157-10.474 0-4.4-1.153-7.817-3.46-10.25-2.307-2.434-5.83-3.65-10.568-3.65-4.147 0-7.554 1.195-10.22 3.585-2.666 2.392-4 5.514-4 9.364H69.908c0-4.74 1.26-9.055 3.776-12.95 2.518-3.892 6.03-6.928 10.537-9.108 4.508-2.18 9.554-3.27 15.14-3.27 9.225 0 16.472 2.318 21.74 6.952 5.27 4.634 7.903 11.077 7.903 19.33 0 4.147-1.323 8.05-3.967 11.71-2.646 3.66-6.062 6.422-10.252 8.284 5.078 1.736 8.94 4.465 11.584 8.19s3.968 8.166 3.968 13.33c0 8.294-2.847 14.895-8.538 19.804s-13.17 7.363-22.438 7.363c-8.887 0-16.166-2.37-21.836-7.11-5.67-4.74-8.506-11.045-8.506-18.916h15.425c0 4.062 1.365 7.363 4.094 9.902 2.73 2.54 6.4 3.81 11.014 3.81 4.782 0 8.55-1.27 11.3-3.81s4.126-6.22 4.126-11.045c0-4.865-1.44-8.61-4.316-11.235-2.878-2.623-7.152-3.936-12.822-3.936H88.76V173.1zM187.598 133.493v12.76h-1.904c-8.633.126-15.53 2.497-20.693 7.108-5.162 4.614-8.23 11.152-9.203 19.615 4.95-5.205 11.277-7.808 18.98-7.808 8.166 0 14.608 2.878 19.328 8.633 4.718 5.755 7.077 13.182 7.077 22.28 0 9.395-2.76 17.002-8.284 22.82-5.52 5.818-12.77 8.73-21.74 8.73-9.226 0-16.705-3.407-22.44-10.222-5.733-6.812-8.6-15.742-8.6-26.787v-5.267c0-16.208 3.945-28.903 11.84-38.086 7.89-9.182 19.242-13.774 34.054-13.774h1.586zM171.03 177.61c-3.386 0-6.485.95-9.3 2.855-2.814 1.904-4.877 4.443-6.188 7.617v4.697c0 6.854 1.438 12.304 4.316 16.345 2.877 4.04 6.602 6.062 11.172 6.062s8.188-1.715 10.854-5.143 4-7.934 4-13.52-1.355-10.135-4.063-13.648c-2.708-3.51-6.304-5.267-10.79-5.267zM271.136 187.447c0 13.29-2.486 23.307-7.46 30.057s-12.535 10.125-22.69 10.125c-9.988 0-17.51-3.292-22.566-9.872-5.058-6.58-7.65-16.323-7.776-29.23V172.53c0-13.287 2.485-23.252 7.458-29.896 4.973-6.643 12.558-9.966 22.757-9.966 10.112 0 17.655 3.237 22.63 9.712 4.97 6.475 7.52 16.166 7.647 29.072v15.995zm-15.425-17.265c0-8.674-1.185-15.033-3.554-19.075-2.37-4.04-6.137-6.062-11.3-6.062-5.035 0-8.738 1.915-11.107 5.745-2.37 3.83-3.62 9.807-3.746 17.932v20.948c0 8.633 1.206 15.064 3.618 19.297s6.2 6.348 11.362 6.348c4.95 0 8.61-1.957 10.98-5.87 2.37-3.915 3.62-10.04 3.746-18.378v-20.885z'/%3E%3C/g%3E%3C/svg%3E");background-size:contain;background-color:rgba(0,0,0,0.5)}.video-js .vjs-big-vr-play-button .vjs-icon-placeholder{display:none}:hover.video-js .vjs-big-vr-play-button{-webkit-transition:border-color 0.4s,outline 0.4s,background-color 0.4s;-moz-transition:border-color 0.4s,outline 0.4s,background-color 0.4s;-ms-transition:border-color 0.4s,outline 0.4s,background-color 0.4s;-o-transition:border-color 0.4s,outline 0.4s,background-color 0.4s;transition:border-color 0.4s,outline 0.4s,background-color 0.4s}.video-js .vjs-big-vr-play-button::before{content:''}.video-js canvas{cursor:move}.video-js .vjs-button-vr .vjs-icon-placeholder{height:30px;width:30px;display:inline-block;background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI0ZGRkZGRiI+CiAgICA8cGF0aCBkPSJNMjAuNzQgNkgzLjIxQzIuNTUgNiAyIDYuNTcgMiA3LjI4djEwLjQ0YzAgLjcuNTUgMS4yOCAxLjIzIDEuMjhoNC43OWMuNTIgMCAuOTYtLjMzIDEuMTQtLjc5bDEuNC0zLjQ4Yy4yMy0uNTkuNzktMS4wMSAxLjQ0LTEuMDFzMS4yMS40MiAxLjQ1IDEuMDFsMS4zOSAzLjQ4Yy4xOS40Ni42My43OSAxLjExLjc5aDQuNzljLjcxIDAgMS4yNi0uNTcgMS4yNi0xLjI4VjcuMjhjMC0uNy0uNTUtMS4yOC0xLjI2LTEuMjh6TTcuNSAxNC42MmMtMS4xNyAwLTIuMTMtLjk1LTIuMTMtMi4xMiAwLTEuMTcuOTYtMi4xMyAyLjEzLTIuMTMgMS4xOCAwIDIuMTIuOTYgMi4xMiAyLjEzcy0uOTUgMi4xMi0yLjEyIDIuMTJ6bTkgMGMtMS4xNyAwLTIuMTMtLjk1LTIuMTMtMi4xMiAwLTEuMTcuOTYtMi4xMyAyLjEzLTIuMTNzMi4xMi45NiAyLjEyIDIuMTMtLjk1IDIuMTItMi4xMiAyLjEyeiIvPgogICAgPHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgyNHYyNEgwVjB6Ii8+Cjwvc3ZnPgo=) no-repeat left center}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512pt" height="512pt" version="1.0" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g><rect x="-.0072516" y=".00056299" width="512.01" height="512.02" fill="#575757" stroke-width=".063019"/><path d="m247.17 455.95c-19.792-0.78921-38.719-4.2564-57.154-10.47-60.968-20.55-108.68-68.579-127-127.86-7.8955-25.538-10.062-53.943-6.2586-82.067 3.7105-27.439 13.603-53.515 29.342-77.344 12.069-18.273 29.138-36.277 47.228-49.816 36.891-27.61 85.944-42.49 132.38-40.157 25.88 1.3001 49.939 6.765 73.106 16.606 8.1948 3.481 20.024 9.6845 27.696 14.525 14.15 8.9272 22.367 15.498 34.482 27.573 13.254 13.211 22.128 24.276 30.398 37.906 7.2081 11.879 14.099 27.15 18.229 40.397 1.5996 5.1305 4.442 16.456 5.6852 22.653 2.3908 11.917 2.6998 15.722 2.7049 33.312 6e-3 18.515-0.46256 24.413-2.9166 36.758-9.3274 46.92-35.58 88.167-74.872 117.64-22.814 17.112-50.027 29.535-78.547 35.858-16.714 3.7059-35.421 5.2453-54.498 4.4846zm-35.1-78.786c-5.3e-4 -0.52647-0.0741-2.0564-0.16311-3.3999l-0.16178-2.4427-4.7018-0.26271c-4.0477-0.22614-4.7968-0.33363-5.3847-0.77253-2.0235-1.5108-1.4679-6.0695 2.2494-18.457 0.8637-2.8781 3.3371-11.321 5.4966-18.762 2.1594-7.4409 5.2002-17.836 6.7573-23.101 1.5571-5.2648 4.1948-14.282 5.8615-20.038 1.6667-5.7562 3.6145-12.4 4.3284-14.764 0.71391-2.3641 3.2583-11.037 5.6542-19.272 4.9475-17.007 8.1626-27.723 8.9438-29.811 0.51852-1.3858 0.54785-1.4139 0.99761-0.95317 0.25486 0.26106 3.8462 7.3667 7.9807 15.79 4.1345 8.4236 13.089 26.573 19.898 40.331 17.188 34.73 37.849 76.578 43.261 87.622l4.5356 9.257 11.359-0.0895c6.2475-0.0492 11.615-0.19623 11.929-0.32672 0.5614-0.23385 0.54167-0.2959-1.3723-4.3176-1.068-2.2442-8.1436-16.601-15.724-31.904-48.687-98.293-61.22-123.86-67.889-138.48-4.7022-10.309-6.9031-14.807-7.7139-15.762-0.82931-0.97742-1.6319-1.0638-2.3704-0.25525-1.1993 1.313-4.1046 10.063-9.3869 28.27-2.0569 7.0899-6.5372 22.425-9.9562 34.077-6.6396 22.629-8.5182 29.037-14.33 48.883-2.0354 6.9495-4.7977 16.369-6.1385 20.931-1.3408 4.5628-4.033 13.81-5.9826 20.549-4.304 14.877-6.136 20.889-7.3886 24.25-2.1371 5.7334-2.5723 6.3292-4.9216 6.7384-0.88855 0.15472-2.4102 0.28196-3.3815 0.28275-2.1993 3e-3 -3.5494 0.36339-4.0558 1.0863-0.42176 0.60215-0.56421 4.8802-0.18251 5.4812 0.20573 0.32388 2.4672 0.37414 23.34 0.51873l8.6151 0.0597-7e-4 -0.95723zm36.751-205.59c4.3282-0.92335 8.4607-4.943 9.4374-9.1796 0.36569-1.5862 0.32543-4.9758-0.077-6.4799-0.85108-3.1813-3.2688-6.291-6.039-7.7675-3.8111-2.0313-9.456-2.0295-13.272 5e-3 -5.9828 3.1888-8.1556 11.089-4.7878 17.408 2.6995 5.0648 8.3611 7.3754 14.738 6.015z" fill="#f0f0f0" stroke-width=".025526"/></g><g transform="matrix(.069892 0 0 -.069892 44.236 474.48)"><path d="m2787 4669c-124-65-123-255 3-319 86-44 196-16 247 62 58 87 26 211-67 258-51 26-132 26-183-1z" fill="#00b6f0" stroke="#00b6f0" stroke-width="4.25"/><path d="m2882 4108c-12-16-63-166-102-303-30-104-101-350-165-565-20-69-58-199-85-290-26-91-64-221-85-290-20-69-58-199-85-290-26-91-64-221-85-290-20-69-57-195-81-280-59-207-93-299-115-310-10-6-35-10-56-10-73 0-84-8-81-54l3-41 228-3 228-2-3 47-3 48-73 3c-66 3-74 5-84 27-13 28 0 104 37 225 13 41 47 156 75 255s66 230 85 290c18 61 56 191 85 290 28 99 66 230 85 290 18 61 56 191 85 290 85 297 123 419 131 429 5 5 17-11 28-35 10-24 192-393 403-819s447-902 523-1058l139-282h168c92 0 168 4 168 8s-75 158-166 342c-588 1183-969 1958-1033 2100-29 63-69 151-89 195-44 95-58 110-80 83z" fill="#575757"/></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -22,7 +22,8 @@
break;
case 'get_youtube_replies':
var load_more = e.getAttribute('data-load-more') !== null;
get_youtube_replies(e, load_more);
var load_replies = e.getAttribute('data-load-replies') !== null;
get_youtube_replies(e, load_more, load_replies);
break;
case 'toggle_parent':
toggle_parent(e);

View File

@ -14,6 +14,7 @@ var options = {
'durationDisplay',
'progressControl',
'remainingTimeDisplay',
'Spacer',
'captionsButton',
'qualitySelector',
'playbackRateMenuButton',
@ -73,6 +74,55 @@ if (location.pathname.startsWith('/embed/')) {
});
}
// Detect mobile users and initalize mobileUi for better UX
// Detection code taken from https://stackoverflow.com/a/20293441
function isMobile() {
try{ document.createEvent("TouchEvent"); return true; }
catch(e){ return false; }
}
if (isMobile()) {
player.mobileUi();
buttons = ["playToggle", "volumePanel", "captionsButton"];
if (video_data.params.quality !== 'dash') {
buttons.push("qualitySelector")
}
// Create new control bar object for operation buttons
const ControlBar = videojs.getComponent("controlBar");
let operations_bar = new ControlBar(player, {
children: [],
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
});
buttons.slice(1).forEach(child => operations_bar.addChild(child))
// Remove operation buttons from primary control bar
primary_control_bar = player.getChild("controlBar");
buttons.forEach(child => primary_control_bar.removeChild(child));
operations_bar_element = operations_bar.el();
operations_bar_element.className += " mobile-operations-bar"
player.addChild(operations_bar)
// Playback menu doesn't work when its initalized outside of the primary control bar
playback_element = document.getElementsByClassName("vjs-playback-rate")[0]
operations_bar_element.append(playback_element)
// The share and http source selector element can't be fetched till the players ready.
player.one("playing", () => {
share_element = document.getElementsByClassName("vjs-share-control")[0]
operations_bar_element.append(share_element)
if (video_data.params.quality === 'dash') {
http_source_selector = document.getElementsByClassName("vjs-http-source-selector vjs-menu-button")[0]
operations_bar_element.append(http_source_selector)
}
})
}
player.on('error', function (event) {
if (player.error().code === 2 || player.error().code === 4) {
setTimeout(function (event) {
@ -98,6 +148,17 @@ player.on('error', function (event) {
}
});
// Enable VR video support
if (!video_data.params.listen && video_data.vr && video_data.params.vr_mode) {
player.crossOrigin("anonymous")
switch (video_data.projection_type) {
case "EQUIRECTANGULAR":
player.vr({projection: "equirectangular"});
default: // Should only be "MESH" but we'll use this as a fallback.
player.vr({projection: "EAC"});
}
}
// Add markers
if (video_data.params.video_start > 0 || video_data.params.video_end > 0) {
var markers = [{ time: video_data.params.video_start, text: 'Start' }];
@ -566,3 +627,20 @@ if (navigator.vendor == "Apple Computer, Inc." && video_data.params.listen) {
});
});
}
// Watch on Invidious link
if (window.location.pathname.startsWith("/embed/")) {
const Button = videojs.getComponent('Button');
let watch_on_invidious_button = new Button(player);
// Create hyperlink for current instance
redirect_element = document.createElement("a");
redirect_element.setAttribute("href", `http://${window.location.host}/watch?v=${window.location.pathname.replace("/embed/","")}`)
redirect_element.appendChild(document.createTextNode("Invidious"))
watch_on_invidious_button.el().appendChild(redirect_element)
watch_on_invidious_button.addClass("watch-on-invidious")
cb = player.getChild('ControlBar')
cb.addChild(watch_on_invidious_button)
};

7
assets/js/videojs-mobile-ui.min.js vendored Normal file
View File

@ -0,0 +1,7 @@
/**
* videojs-mobile-ui
* @version 0.5.2
* @copyright 2021 mister-ben <git@misterben.me>
* @license MIT
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("video.js"),require("global/window")):"function"==typeof define&&define.amd?define(["video.js","global/window"],t):e.videojsMobileUi=t(e.videojs,e.window)}(this,function(e,t){"use strict";e=e&&e.hasOwnProperty("default")?e.default:e,t=t&&t.hasOwnProperty("default")?t.default:t;var n=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},o=function(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t},i=e.getComponent("Component"),r=e.dom||e,a=function(e){function i(t,r){n(this,i);var a=o(this,e.call(this,t,r));return a.seekSeconds=r.seekSeconds,a.tapTimeout=r.tapTimeout,a.addChild("playToggle",{}),t.on(["playing","userinactive"],function(e){a.removeClass("show-play-toggle")}),0===a.player_.options_.inactivityTimeout&&(a.player_.options_.inactivityTimeout=5e3),a.enable(),a}return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}(i,e),i.prototype.createEl=function(){return r.createEl("div",{className:"vjs-touch-overlay",tabIndex:-1})},i.prototype.handleTap=function(e){var n=this;e.target===this.el_&&(e.preventDefault(),this.firstTapCaptured?(this.firstTapCaptured=!1,this.timeout&&t.clearTimeout(this.timeout),this.handleDoubleTap(e)):(this.firstTapCaptured=!0,this.timeout=t.setTimeout(function(){n.firstTapCaptured=!1,n.handleSingleTap(e)},this.tapTimeout)))},i.prototype.handleSingleTap=function(e){this.removeClass("skip"),this.toggleClass("show-play-toggle")},i.prototype.handleDoubleTap=function(e){var n=this,o=this.el_.getBoundingClientRect(),i=e.changedTouches[0].clientX-o.left;if(i<.4*o.width)this.player_.currentTime(Math.max(0,this.player_.currentTime()-this.seekSeconds)),this.addClass("reverse");else{if(!(i>o.width-.4*o.width))return;this.player_.currentTime(Math.min(this.player_.duration(),this.player_.currentTime()+this.seekSeconds)),this.removeClass("reverse")}this.removeClass("show-play-toggle"),this.removeClass("skip"),t.requestAnimationFrame(function(){n.addClass("skip")})},i.prototype.enable=function(){this.firstTapCaptured=!1,this.on("touchend",this.handleTap)},i.prototype.disable=function(){this.off("touchend",this.handleTap)},i}(i);i.registerComponent("TouchOverlay",a);var s={fullscreen:{enterOnRotate:!0,exitOnRotate:!0,lockOnRotate:!0,iOS:!1},touchControls:{seekSeconds:10,tapTimeout:300,disableOnEnd:!1}},l=t.screen,u=function(n,o){n.addClass("vjs-mobile-ui"),(o.touchControls.disableOnEnd||"function"==typeof n.endscreen)&&n.addClass("vjs-mobile-ui-disable-end"),o.fullscreen.iOS&&e.browser.IS_IOS&&e.browser.IOS_VERSION>9&&!n.el_.ownerDocument.querySelector(".bc-iframe")&&(n.tech_.el_.setAttribute("playsinline","playsinline"),n.tech_.supportsFullScreen=function(){return!1});var i=void 0,r=e.VERSION.split("."),a=parseInt(r[0],10),s=parseInt(r[1],10);i=a<7||7===a&&s<7?Array.prototype.indexOf.call(n.el_.children,n.getChild("ControlBar").el_):n.children_.indexOf(n.getChild("ControlBar")),n.addChild("TouchOverlay",o.touchControls,i);var u=!1,c=function(){var i="number"==typeof t.orientation?t.orientation:l&&l.orientation&&l.orientation.angle?t.orientation:(e.log("angle unknown"),0);90!==i&&270!==i&&-90!==i||!o.enterOnRotate||!1===n.paused()&&(n.requestFullscreen(),o.fullscreen.lockOnRotate&&l.orientation&&l.orientation.lock&&l.orientation.lock("landscape").then(function(){u=!0}).catch(function(){e.log("orientation lock not allowed")})),0!==i&&180!==i||!o.exitOnRotate||n.isFullscreen()&&n.exitFullscreen()};e.browser.IS_IOS?t.addEventListener("orientationchange",c):l.orientation&&(l.orientation.onchange=c),n.on("ended",function(e){!0===u&&(l.orientation.unlock(),u=!1)})},c=function(){var t=this,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(n.forceForTesting||e.browser.IS_ANDROID||e.browser.IS_IOS)&&this.ready(function(){u(t,e.mergeOptions(s,n))})};return(e.registerPlugin||e.plugin)("mobileUi",c),c.VERSION="0.5.2",c});

52730
assets/js/videojs-vr.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -359,7 +359,7 @@ function get_youtube_comments(retries) {
xhr.send();
}
function get_youtube_replies(target, load_more) {
function get_youtube_replies(target, load_more, load_replies) {
var continuation = target.getAttribute('data-continuation');
var body = target.parentNode.parentNode;
@ -371,7 +371,10 @@ function get_youtube_replies(target, load_more) {
'?format=html' +
'&hl=' + video_data.preferences.locale +
'&thin_mode=' + video_data.preferences.thin_mode +
'&continuation=' + continuation;
'&continuation=' + continuation
if (load_replies) {
url += '&action=action_get_comment_replies';
}
var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.timeout = 10000;

View File

@ -1,3 +1,2 @@
User-agent: *
Disallow: /search
Disallow: /login
Disallow: /

View File

@ -1,13 +1,825 @@
channel_threads: 1
feed_threads: 1
#########################################
#
# Database configuration
#
#########################################
##
## Database configuration with separate parameters.
## This setting is MANDATORY, unless 'database_url' is used.
##
db:
user: kemal
password: kemal
host: localhost
port: 5432
dbname: invidious
# alternatively, the database URL can be provided directly - if both are set then the latter takes precedence
# database_url: postgres://kemal:kemal@localhost:5432/invidious
full_refresh: false
https_only: false
##
## Database configuration using a single URI. This is an
## alternative to the 'db' parameter above. If both forms
## are used, then only database_url is used.
## This setting is MANDATORY, unless 'db' is used.
##
## Note: The 'database_url' setting allows the use of UNIX
## sockets. To do so, remove the IP address (or FQDN) and port
## and append the 'host' parameter. E.g:
## postgres://kemal:kemal@/invidious?host=/var/run/postgresql
##
## Accepted values: a postgres:// URI
## Default: postgres://kemal:kemal@localhost:5432/invidious
##
#database_url: postgres://kemal:kemal@localhost:5432/invidious
##
## Enable automatic table integrity check. This will create
## the required tables and columns if anything is missing.
##
## Accepted values: true, false
## Default: false
##
#check_tables: false
#########################################
#
# Server config
#
#########################################
# -----------------------------
# Network (inbound)
# -----------------------------
##
## Port to listen on for incoming connections.
##
## Note: Ports lower than 1024 requires either root privileges
## (not recommended) or the "CAP_NET_BIND_SERVICE" capability
## (See https://stackoverflow.com/a/414258 and `man capabilities`)
##
## Accepted values: 1-65535
## Default: 3000
##
#port: 3000
##
## When the invidious instance is behind a proxy, and the proxy
## listens on a different port than the instance does, this lets
## invidious know about it. This is used to craft absolute URLs
## to the instance (e.g in the API).
##
## Note: This setting is MANDATORY if invidious is behind a
## reverse proxy.
##
## Accepted values: 1-65535
## Default: <none>
##
#external_port:
##
## Interface address to listen on for incoming connections.
##
## Accepted values: a valid IPv4 or IPv6 address.
## default: 0.0.0.0 (listen on all interfaces)
##
#host_binding: 0.0.0.0
##
## Domain name under which this instance is hosted. This is
## used to craft absolute URLs to the instance (e.g in the API).
## The domain MUST be defined if your instance is accessed from
## a domain name (like 'example.com').
##
## Accepted values: a fully qualified domain name (FQDN)
## Default: <none>
##
domain:
##
## Tell Invidious that it is behind a proxy that provides only
## HTTPS, so all links must use the https:// scheme. This
## setting MUST be set to true if invidious is behind a
## reverse proxy serving HTTPs.
##
## Accepted values: true, false
## Default: false
##
https_only: false
##
## Enable/Disable 'Strict-Transport-Security'. Make sure that
## the domain specified under 'domain' is served securely.
##
## Accepted values: true, false
## Default: true
##
#hsts: true
# -----------------------------
# Network (outbound)
# -----------------------------
##
## Disable proxying server-wide. Can be disable as a whole, or
## only for a single function.
##
## Accepted values: true, false, dash, livestreams, downloads, local
## Default: false
##
#disable_proxy: false
##
## Size of the HTTP pool used to connect to youtube. Each
## domain ('youtube.com', 'ytimg.com', ...) has its own pool.
##
## Accepted values: a positive integer
## Default: 100
##
#pool_size: 100
##
## Enable/Disable the use of QUIC (HTTP/3) when connecting
## to the youtube API and websites ('youtube.com', 'ytimg.com').
## QUIC's main advantages are its lower latency and lower bandwidth
## use, compared to its predecessors. However, the current version
## of QUIC used in invidious is still based on the IETF draft 31,
## meaning that the underlying library may still not be fully
## optimized. You can read more about QUIC at the link below:
## https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-31
##
## Note: you should try both options and see what is the best for your
## instance. In general QUIC is recommended for public instances. Your
## mileage may vary.
##
## Note 2: Using QUIC prevents some captcha challenges from appearing.
## See: https://github.com/iv-org/invidious/issues/957#issuecomment-576424042
##
## Accepted values: true, false
## Default: true
##
#use_quic: true
##
## Additionnal cookies to be sent when requesting the youtube API.
##
## Accepted values: a string in the format "name1=value1; name2=value2..."
## Default: <none>
##
#cookies:
##
## Force connection to youtube over a specific IP family.
##
## Note: This may sometimes resolve issues involving rate-limiting.
## See https://github.com/ytdl-org/youtube-dl/issues/21729.
##
## Accepted values: ipv4, ipv6
## Default: <none>
##
#force_resolve:
# -----------------------------
# Logging
# -----------------------------
##
## Path to log file. Can be absolute or relative to the invidious
## binary. This is overriden if "-o OUTPUT" or "--output=OUTPUT"
## are passed on the command line.
##
## Accepted values: a filesystem path or 'STDOUT'
## Default: STDOUT
##
#output: STDOUT
##
## Logging Verbosity. This is overriden if "-l LEVEL" or
## "--log-level=LEVEL" are passed on the command line.
##
## Accepted values: All, Trace, Debug, Info, Warn, Error, Fatal, Off
## Default: Info
##
#log_level: Info
# -----------------------------
# Features
# -----------------------------
##
## Enable/Disable the "Popular" tab on the main page.
##
## Accepted values: true, false
## Default: true
##
#popular_enabled: true
##
## Enable/Disable statstics (available at /api/v1/stats).
## The following data is available:
## - Software name ("invidious") and version+branch (same data as
## displayed in the footer, e.g: "2021.05.13-75e5b49" / "master")
## - The value of the 'registration_enabled' config (true/false)
## - Number of currently registered users
## - Number of registered users who connected in the last month
## - Number of registered users who connected in the last 6 months
## - Timestamp of the last server restart
## - Timestamp of the last "Channel Refresh" job execution
##
## Warning: This setting MUST be set to true if you plan to run
## a public instance. It is used by api.invidious.io to refresh
## your instance's status.
##
## Accepted values: true, false
## Default: false
##
#statistics_enabled: false
# -----------------------------
# Users and accounts
# -----------------------------
##
## Allow/Forbid Invidious (local) account creation. Invidious
## accounts allow users to subscribe to channels and to create
## playlists without a Google account.
##
## Accepted values: true, false
## Default: true
##
#registration_enabled: true
##
## Allow/Forbid users to log-in. This setting affects the ability
## to connect with BOTH Google and Invidious (local) accounts.
##
## Accepted values: true, false
## Default: true
##
#login_enabled: true
##
## Enable/Disable the captcha challenge on the login page.
##
## Note: this is a basic captcha challenge that doesn't
## depend on any third parties.
##
## Accepted values: true, false
## Default: true
##
#captcha_enabled: true
##
## List of usernames that will be granted administrator rights.
## A user with administrator rights will be able to change the
## server configuration options listed below in /preferences,
## in addition to the usual user preferences.
##
## Server-wide settings:
## - popular_enabled
## - captcha_enabled
## - login_enabled
## - registration_enabled
## - statistics_enabled
## Default user preferences:
## - default_home
## - feed_menu
##
## Accepted values: an array of strings
## Default: [""]
##
#admins: [""]
# -----------------------------
# Background jobs
# -----------------------------
##
## Number of threads to use when crawling channel videos (during
## subscriptions update).
##
## Notes:
## - Setting this to 0 will disable the channel videos crawl job.
## - This setting is overriden if "-c THREADS" or
## "--channel-threads=THREADS" are passed on the command line.
##
## Accepted values: a positive integer
## Default: 1
##
channel_threads: 1
##
## Forcefully dump and re-download the entire list of uploaded
## videos when crawling channel (during subscriptions update).
##
## Accepted values: true, false
## Default: false
##
full_refresh: false
##
## Number of threads to use when updating RSS feeds.
##
## Notes:
## - Setting this to 0 will disable the channel videos crawl job.
## - This setting is overriden if "-f THREADS" or
## "--feed-threads=THREADS" are passed on the command line.
##
## Accepted values: a positive integer
## Default: 1
##
feed_threads: 1
##
## Enable/Disable the polling job that keeps the decryption
## function (for "secured" videos) up to date.
##
## Note: This part of the code is currently broken, so changing
## this setting has no impact.
##
## Accepted values: true, false
## Default: true
##
#decrypt_polling: true
# -----------------------------
# Captcha API
# -----------------------------
##
## URL of the captcha solving service.
##
## Accepted values: any URL
## Default: https://api.anti-captcha.com
##
#captcha_api_url: https://api.anti-captcha.com
##
## API key for the captcha solving service.
##
## Accepted values: a string
## Default: <none>
##
#captcha_key:
# -----------------------------
# Miscellanous
# -----------------------------
##
## custom banner displayed at the top of every page. This can
## used for instance announcements, e.g.
##
## Accepted values: any string. HTML is accepted.
## Default: <none>
##
#banner:
##
## Subscribe to channels using PubSubHub (Google PubSubHubbub service).
## PubSubHub allows Invidious to be instantly notified when a new video
## is published on any subscribed channels. When PubSubHub is not used,
## Invidious will check for new videos every minute.
##
## Note: This setting is recommended for public instances.
##
## Note 2:
## - Requires a public instance (it uses /feed/webhook/v1)
## - Requires 'domain' and 'hmac_key' to be set.
## - Setting this parameter to any number greater than zero will
## enable channel subscriptions via PubSubHub, but will limit the
## amount of concurrent subscriptions.
##
## Accepted values: true, false, a positive integer
## Default: false
##
#use_pubsub_feeds: false
##
## HMAC signing key used for CSRF tokens and pubsub
## subscriptions verification.
##
## Accepted values: a string
## Default: <none>
##
#hmac_key:
##
## List of video IDs where the "download" widget must be
## disabled, in order to comply with DMCA requests.
##
## Accepted values: an array of string
## Default: <none>
##
#dmca_content:
##
## Cache video annotations in the database.
##
## Warning: empty annotations or annotations that only contain
## cards won't be cached.
##
## Accepted values: true, false
## Default: false
##
#cache_annotations: false
#########################################
#
# Default user preferences
#
#########################################
##
## NOTE: All the settings below define the default user
## preferences. They will apply to ALL users connecting
## without a preferences cookie (so either on the first
## connection to the instance or after clearing the
## browser's cookies).
##
default_user_preferences:
# -----------------------------
# Internationalization
# -----------------------------
##
## Default user interface language (locale).
##
## Note: overridin the default (no preferred caption language)
## is not recommended, in order to not penalize people using
## other languages.
##
## Accepted values:
## ar (Arabic)
## da (Danish)
## de (German)
## en-US (english, US)
## el (Greek)
## eo (Esperanto)
## es (Spanish)
## fa (Persian)
## fi (Finnish)
## fr (French)
## he (Hebrew)
## hr (Hungarian)
## id (Indonesian)
## is (Icelandic)
## it (Italian)
## ja (Japanese)
## nb-NO (Norwegian, Bokmål)
## nl (Dutch)
## pl (Polish)
## pt-BR (Portuguese, Brazil)
## pt-PT (Portuguese, Portugal)
## ro (Romanian)
## ru (Russian)
## sv (Swedish)
## tr (Turkish)
## uk (Ukrainian)
## zh-CN (Chinese, China) (a.k.a "Simplified Chinese")
## zh-TW (Chinese, Taiwan) (a.k.a "Traditional Chinese")
##
## Default: en-US
##
#locale: en-US
##
## Top 3 prefered languages for video captions.
##
## Note: overridin the default (no preferred
## caption language) is not recommended, in order
## to not penalize people using other languages.
##
## Accepted values: a three-entries array.
## Each entry can be one of:
## "English", "English (auto-generated)",
## "Afrikaans", "Albanian", "Amharic", "Arabic",
## "Armenian", "Azerbaijani", "Bangla", "Basque",
## "Belarusian", "Bosnian", "Bulgarian", "Burmese",
## "Catalan", "Cebuano", "Chinese (Simplified)",
## "Chinese (Traditional)", "Corsican", "Croatian",
## "Czech", "Danish", "Dutch", "Esperanto", "Estonian",
## "Filipino", "Finnish", "French", "Galician", "Georgian",
## "German", "Greek", "Gujarati", "Haitian Creole", "Hausa",
## "Hawaiian", "Hebrew", "Hindi", "Hmong", "Hungarian",
## "Icelandic", "Igbo", "Indonesian", "Irish", "Italian",
## "Japanese", "Javanese", "Kannada", "Kazakh", "Khmer",
## "Korean", "Kurdish", "Kyrgyz", "Lao", "Latin", "Latvian",
## "Lithuanian", "Luxembourgish", "Macedonian",
## "Malagasy", "Malay", "Malayalam", "Maltese", "Maori",
## "Marathi", "Mongolian", "Nepali", "Norwegian Bokmål",
## "Nyanja", "Pashto", "Persian", "Polish", "Portuguese",
## "Punjabi", "Romanian", "Russian", "Samoan",
## "Scottish Gaelic", "Serbian", "Shona", "Sindhi",
## "Sinhala", "Slovak", "Slovenian", "Somali",
## "Southern Sotho", "Spanish", "Spanish (Latin America)",
## "Sundanese", "Swahili", "Swedish", "Tajik", "Tamil",
## "Telugu", "Thai", "Turkish", "Ukrainian", "Urdu",
## "Uzbek", "Vietnamese", "Welsh", "Western Frisian",
## "Xhosa", "Yiddish", "Yoruba", "Zulu"
##
## Default: ["", "", ""]
##
#captions: ["", "", ""]
# -----------------------------
# Interface
# -----------------------------
##
## Enable/Disable dark mode.
##
## Accepted values: true, false
## Default: <none>
##
#dark_mode:
##
## Enable/Disable thin mode (no video thumbnails).
##
## Accepted values: true, false
## Default: false
##
#thin_mode: false
##
## List of feeds available on the home page.
##
## Note: "Subscriptions" and "Playlists" are only visible
## when the user is logged in.
##
## Accepted values: A list of strings
## Each entry can be one of: "Popular", "Trending",
## "Subscriptions", "Playlists"
##
## Default: ["Popular", "Trending", "Subscriptions", "Playlists"] (show all feeds)
##
#feed_menu: ["Popular", "Trending", "Subscriptions", "Playlists"]
##
## Default feed to diplay on the home page.
##
## Note: setting this option to "Popular" has no
## effect when 'popular_enabled' is set to false.
##
## Accepted values: Popular, Trending, Subscriptions, Playlists, <none>
## Default: Popular
##
#default_home: Popular
##
## Default number of results to display per page.
##
## Note: this affects invidious-generated pages only, such
## as watch history and subscription feeds. Playlists, search
## results and channel videos depend on the data returned by
## the Youtube API.
##
## Accepted values: any positive integer
## Default: 40
##
#max_results: 40
##
## Show/hide annotations.
##
## Accepted values: true, false
## Default: false
##
#annotations: false
##
## Show/hide annotation.
##
## Accepted values: true, false
## Default: false
##
#annotations_subscribed: false
##
## Type of comments to display below video.
##
## Accepted values: a two-entries array.
## Each entry can be one of: "youtube", "reddit", ""
##
## Default: ["youtube", ""]
##
#comments: ["youtube", ""]
##
## Default player style.
##
## Accepted values: invidious, youtube
## Default: invidious
##
#player_style: invidious
##
## Show/Hide the "related videos" sidebar when
## watching a video.
##
## Accepted values: true, false
## Default: true
##
#related_videos: true
# -----------------------------
# Video player behavior
# -----------------------------
##
## Automatically play videos on page load.
##
## Accepted values: true, false
## Default: false
##
#autoplay: false
##
## Automatically load the "next" video (either next in
## playlist or proposed) when the current video ends.
##
## Accepted values: true, false
## Default: false
##
#continue: false
##
## Autoplay next video by default.
##
## Note: Only effective if 'continue' is set to true.
##
## Accepted values: true, false
## Default: true
##
#continue_autoplay: true
##
## Play videos in Audio-only mode by default.
##
## Accepted values: true, false
## Default: false
##
#listen: false
##
## Loop videos automatically.
##
## Accepted values: true, false
## Default: false
##
#video_loop: false
# -----------------------------
# Video playback settings
# -----------------------------
##
## Default video quality.
##
## Accepted values: dash, hd720, medium, small
## Default: hd720
##
#quality: hd720
##
## Default dash video quality.
##
## Note: this setting only takes effet if the
## 'quality' parameter is set to "dash".
##
## Accepted values:
## auto, best, 4320p, 2160p, 1440p, 1080p,
## 720p, 480p, 360p, 240p, 144p, worst
## Default: auto
##
#quality_dash: auto
##
## Default video playback speed.
##
## Accepted values: 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0
## Default: 1.0
##
#speed: 1.0
##
## Default volume.
##
## Accepted values: 0-100
## Default: 100
##
#volume: 100
##
## Allow 360° videos to be played.
##
## Note: This feature requires a WebGL-enabled browser.
##
## Accepted values: true, false
## Default: true
##
#vr_mode: true
# -----------------------------
# Subscription feed
# -----------------------------
##
## In the "Subscription" feed, only show the latest video
## of each channel the user is subscribed to.
##
## Note: when combined with 'unseen_only', the latest unseen
## video of each channel will be displayed instead of the
## latest by date.
##
## Accepted values: true, false
## Default: false
##
#latest_only: false
##
## Enable/Disable user subscriptions desktop notifications.
##
## Accepted values: true, false
## Default: false
##
#notifications_only: false
##
## In the "Subscription" feed, Only show the videos that the
## user haven't watched yet (i.e which are not in their watch
## history).
##
## Accepted values: true, false
## Default: false
##
#unseen_only: false
##
## Default sorting parameter for subscription feeds.
##
## Accepted values:
## 'alphabetically'
## 'alphabetically - reverse'
## 'channel name'
## 'channel name - reverse'
## 'published'
## 'published - reverse'
##
## Default: published
##
#sort: published
# -----------------------------
# Miscellanous
# -----------------------------
##
## Proxy videos through instance by default.
##
## Warning: As most users won't change this setting in their
## preferences, defaulting to true will significantly
## increase the instance's network usage, so make sure that
## your server's connection can handle it.
##
## Accepted values: true, false
## Default: false
##
#local: false
##
## Show the connected user's nick at the top right.
##
## Accepted values: true, false
## Default: true
##
#show_nick: true
##
## Automatically redirect to a random instance when the user uses
## any "switch invidious instance" link (For videos, it's the plane
## icon, next to "watch on youtube" and "listen"). When set to false,
## the user is sent to https://redirect.invidious.io instead, where
## they can manually select an instance.
##
## Accepted values: true, false
## Default: false
##
#automatic_instance_redirect: false

View File

@ -2,11 +2,11 @@
-- DROP TABLE public.annotations;
CREATE TABLE public.annotations
CREATE TABLE IF NOT EXISTS public.annotations
(
id text NOT NULL,
annotations xml,
CONSTRAINT annotations_id_key UNIQUE (id)
);
GRANT ALL ON TABLE public.annotations TO kemal;
GRANT ALL ON TABLE public.annotations TO current_user;

View File

@ -2,7 +2,7 @@
-- DROP TABLE public.channel_videos;
CREATE TABLE public.channel_videos
CREATE TABLE IF NOT EXISTS public.channel_videos
(
id text NOT NULL,
title text,
@ -17,13 +17,13 @@ CREATE TABLE public.channel_videos
CONSTRAINT channel_videos_id_key UNIQUE (id)
);
GRANT ALL ON TABLE public.channel_videos TO kemal;
GRANT ALL ON TABLE public.channel_videos TO current_user;
-- Index: public.channel_videos_ucid_idx
-- DROP INDEX public.channel_videos_ucid_idx;
CREATE INDEX channel_videos_ucid_idx
CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx
ON public.channel_videos
USING btree
(ucid COLLATE pg_catalog."default");

View File

@ -2,7 +2,7 @@
-- DROP TABLE public.channels;
CREATE TABLE public.channels
CREATE TABLE IF NOT EXISTS public.channels
(
id text NOT NULL,
author text,
@ -12,13 +12,13 @@ CREATE TABLE public.channels
CONSTRAINT channels_id_key UNIQUE (id)
);
GRANT ALL ON TABLE public.channels TO kemal;
GRANT ALL ON TABLE public.channels TO current_user;
-- Index: public.channels_id_idx
-- DROP INDEX public.channels_id_idx;
CREATE INDEX channels_id_idx
CREATE INDEX IF NOT EXISTS channels_id_idx
ON public.channels
USING btree
(id COLLATE pg_catalog."default");

View File

@ -2,20 +2,20 @@
-- DROP TABLE public.nonces;
CREATE TABLE public.nonces
CREATE TABLE IF NOT EXISTS public.nonces
(
nonce text,
expire timestamp with time zone,
CONSTRAINT nonces_id_key UNIQUE (nonce)
);
GRANT ALL ON TABLE public.nonces TO kemal;
GRANT ALL ON TABLE public.nonces TO current_user;
-- Index: public.nonces_nonce_idx
-- DROP INDEX public.nonces_nonce_idx;
CREATE INDEX nonces_nonce_idx
CREATE INDEX IF NOT EXISTS nonces_nonce_idx
ON public.nonces
USING btree
(nonce COLLATE pg_catalog."default");

View File

@ -2,7 +2,7 @@
-- DROP TABLE public.playlist_videos;
CREATE TABLE playlist_videos
CREATE TABLE IF NOT EXISTS playlist_videos
(
title text,
id text,
@ -16,4 +16,4 @@ CREATE TABLE playlist_videos
PRIMARY KEY (index,plid)
);
GRANT ALL ON TABLE public.playlist_videos TO kemal;
GRANT ALL ON TABLE public.playlist_videos TO current_user;

View File

@ -13,7 +13,7 @@ CREATE TYPE public.privacy AS ENUM
-- DROP TABLE public.playlists;
CREATE TABLE public.playlists
CREATE TABLE IF NOT EXISTS public.playlists
(
title text,
id text primary key,
@ -26,4 +26,4 @@ CREATE TABLE public.playlists
index int8[]
);
GRANT ALL ON public.playlists TO kemal;
GRANT ALL ON public.playlists TO current_user;

View File

@ -2,7 +2,7 @@
-- DROP TABLE public.session_ids;
CREATE TABLE public.session_ids
CREATE TABLE IF NOT EXISTS public.session_ids
(
id text NOT NULL,
email text,
@ -10,13 +10,13 @@ CREATE TABLE public.session_ids
CONSTRAINT session_ids_pkey PRIMARY KEY (id)
);
GRANT ALL ON TABLE public.session_ids TO kemal;
GRANT ALL ON TABLE public.session_ids TO current_user;
-- Index: public.session_ids_id_idx
-- DROP INDEX public.session_ids_id_idx;
CREATE INDEX session_ids_id_idx
CREATE INDEX IF NOT EXISTS session_ids_id_idx
ON public.session_ids
USING btree
(id COLLATE pg_catalog."default");

View File

@ -2,7 +2,7 @@
-- DROP TABLE public.users;
CREATE TABLE public.users
CREATE TABLE IF NOT EXISTS public.users
(
updated timestamp with time zone,
notifications text[],
@ -16,13 +16,13 @@ CREATE TABLE public.users
CONSTRAINT users_email_key UNIQUE (email)
);
GRANT ALL ON TABLE public.users TO kemal;
GRANT ALL ON TABLE public.users TO current_user;
-- Index: public.email_unique_idx
-- DROP INDEX public.email_unique_idx;
CREATE UNIQUE INDEX email_unique_idx
CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx
ON public.users
USING btree
(lower(email) COLLATE pg_catalog."default");

View File

@ -2,7 +2,7 @@
-- DROP TABLE public.videos;
CREATE TABLE public.videos
CREATE TABLE IF NOT EXISTS public.videos
(
id text NOT NULL,
info text,
@ -10,13 +10,13 @@ CREATE TABLE public.videos
CONSTRAINT videos_pkey PRIMARY KEY (id)
);
GRANT ALL ON TABLE public.videos TO kemal;
GRANT ALL ON TABLE public.videos TO current_user;
-- Index: public.id_idx
-- DROP INDEX public.id_idx;
CREATE UNIQUE INDEX id_idx
CREATE UNIQUE INDEX IF NOT EXISTS id_idx
ON public.videos
USING btree
(id COLLATE pg_catalog."default");

View File

@ -12,7 +12,7 @@ services:
POSTGRES_PASSWORD: kemal
POSTGRES_USER: kemal
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
invidious:
build:
context: .

View File

@ -1,18 +1,35 @@
FROM crystallang/crystal:0.36.1-alpine AS builder
RUN apk add --no-cache curl sqlite-static yaml-static
FROM crystallang/crystal:1.1.1-alpine AS builder
RUN apk add --no-cache sqlite-static yaml-static
ARG release
WORKDIR /invidious
COPY ./shard.yml ./shard.yml
COPY ./shard.lock ./shard.lock
RUN shards install && \
curl -Lo ./lib/lsquic/src/lsquic/ext/liblsquic.a https://github.com/iv-org/lsquic-static-alpine/releases/download/v2.18.1/liblsquic.a
RUN shards install
COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a
COPY ./src/ ./src/
# TODO: .git folder is required for building this is destructive.
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
COPY ./.git/ ./.git/
RUN crystal build ./src/invidious.cr \
--static --warnings all \
RUN crystal spec --warnings all \
--link-flags "-lxml2 -llzma"
RUN if [ ${release} == 1 ] ; then \
crystal build ./src/invidious.cr \
--release \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
else \
crystal build ./src/invidious.cr \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
fi
FROM alpine:latest
RUN apk add --no-cache librsvg ttf-opensans
WORKDIR /invidious
@ -25,6 +42,7 @@ RUN sed -i 's/host: \(127.0.0.1\|localhost\)/host: postgres/' config/config.yml
COPY ./config/sql/ ./config/sql/
COPY ./locales/ ./locales/
COPY --from=builder /invidious/invidious .
RUN chmod o+rX -R ./assets ./config ./locales
EXPOSE 3000
USER invidious

48
docker/Dockerfile.arm64 Normal file
View File

@ -0,0 +1,48 @@
FROM alpine:edge AS builder
RUN apk add --no-cache 'crystal=1.1.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev
ARG release
WORKDIR /invidious
COPY ./shard.yml ./shard.yml
COPY ./shard.lock ./shard.lock
RUN shards install
COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a
COPY ./src/ ./src/
# TODO: .git folder is required for building this is destructive.
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
COPY ./.git/ ./.git/
RUN crystal spec --warnings all \
--link-flags "-lxml2 -llzma"
RUN if [ ${release} == 1 ] ; then \
crystal build ./src/invidious.cr \
--release \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
else \
crystal build ./src/invidious.cr \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
fi
FROM alpine:edge
RUN apk add --no-cache librsvg ttf-opensans
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious
COPY ./assets/ ./assets/
COPY --chown=invidious ./config/config.* ./config/
RUN mv -n config/config.example.yml config/config.yml
RUN sed -i 's/host: \(127.0.0.1\|localhost\)/host: postgres/' config/config.yml
COPY ./config/sql/ ./config/sql/
COPY ./locales/ ./locales/
COPY --from=builder /invidious/invidious .
RUN chmod o+rX -R ./assets ./config ./locales
EXPOSE 3000
USER invidious
CMD [ "/invidious/invidious" ]

View File

@ -5,12 +5,12 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
CREATE USER postgres;
EOSQL
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channels.sql
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/videos.sql
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channel_videos.sql
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/users.sql
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/session_ids.sql
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/nonces.sql
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channels.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/videos.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channel_videos.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/users.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/session_ids.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/nonces.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql

View File

@ -1,7 +1,7 @@
name: invidious
image:
repository: iv-org/invidious
repository: quay.io/invidious/invidious
tag: latest
pullPolicy: Always

View File

@ -1,73 +1,84 @@
{
"`x` subscribers": "`x` المشتركين",
"`x` videos": "`x` الفيديوهات",
"`x` playlists": "`x` قوائم التشغيل",
"LIVE": "مباشر",
"Shared `x` ago": "تم رفع الفيديو منذ `x`",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` المشتركين",
"": "`x` المشتركين"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` المقاطع المرئيَّة",
"": "`x` المقاطع المرئيَّة"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` قوائم التشغيل",
"": "`x` قوائم التشغيل"
},
"LIVE": "مُباشِر",
"Shared `x` ago": "تمَّ رفع المقطع المرئيّ مُنذ `x`",
"Unsubscribe": "إلغاء الإشتراك",
"Subscribe": "إشتراك",
"Subscribe": "الإشتراك",
"View channel on YouTube": "زيارة القناة على موقع يوتيوب",
"View playlist on YouTube": "عرض قائمة التشغيل على اليوتيوب",
"newest": "الأجدد",
"oldest": "الأقدم",
"popular": "الأكثر شعبية",
"last": "اخر قوائم التشغيل المعدلة",
"Next page": "الصفحة الثانية",
"last": "الأخيرة",
"Next page": "الصفحة التالية",
"Previous page": "الصفحة السابقة",
"Clear watch history?": "مسح السجل ؟",
"New password": "الرقم السرى الجديد",
"New passwords must match": "الأرقام السرية يجب ان تكون متطابقة",
"Cannot change password for Google accounts": "لا يستطيع تغيير الرقم السرى لحساب جوجل",
"Authorize token?": "رمز الإذن ؟",
"Authorize token for `x`?": "تصريح الرمز لـ `x` ؟",
"Clear watch history?": "هل تريد محو سجل المشاهدة؟",
"New password": "كلمة مرور جديدة",
"New passwords must match": "يَجبُ أن تكون كلمتي المرور متطابقتان",
"Cannot change password for Google accounts": "لا يُمكن تغيير كلمة المرور لِحسابات جوجل",
"Authorize token?": "رمز التفويض؟",
"Authorize token for `x`?": "رمز التفويض لـ `x` ؟",
"Yes": "نعم",
"No": "لا",
"Import and Export Data": "استخراج و إضافة البيانات",
"Import": "إضافة",
"Import Invidious data": "إضافة بيانات Invidious",
"Import YouTube subscriptions": "إضافةالإشتراكات من موقع يوتيوب",
"Import FreeTube subscriptions (.db)": "إضافةالمشتركين من FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "إضافة المشتركين من NewPipe (.json)",
"Import NewPipe data (.zip)": "إضافة بيانات NewPipe (.zip)",
"Export": "استخراج",
"Export subscriptions as OPML": "استخراج المشتركين كـ OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "استخراج المشتركين كـ OPML (لـ NewPipe و FreeTube)",
"Export data as JSON": "استخراج البيانات كـ JSON",
"Delete account?": "حذف الحساب ؟",
"History": "السجل",
"An alternative front-end to YouTube": "البديل الكامل لموقع يوتيوب",
"JavaScript license information": "معلومات ترخيص JavaScript",
"Import and Export Data": "اِستيراد البيانات وتصديرها",
"Import": "استيراد",
"Import Invidious data": "استيراد بيانات انفيدياس",
"Import YouTube subscriptions": "استيراد اشتراكات يوتيوب",
"Import FreeTube subscriptions (.db)": "استيراد اشتراكات فريتيوب (.db)",
"Import NewPipe subscriptions (.json)": "استيراد اشتراكات نيو بايب (.json)",
"Import NewPipe data (.zip)": "استيراد بيانات نيو بايب (.zip)",
"Export": "تصدير",
"Export subscriptions as OPML": "تصدير الاشتراكات كَ OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "تصدير الاشتراكات كَ OPML (لِنيو بايب و فريتيوب)",
"Export data as JSON": "تصدير البيانات بتنسيق JSON",
"Delete account?": "حذف الحساب؟",
"History": "السِّجل",
"An alternative front-end to YouTube": "واجهة أمامية بديلة لموقع يوتيوب",
"JavaScript license information": "معلومات ترخيص جافا سكربت",
"source": "المصدر",
"Log in": "تسجيل الدخول",
"Log in/register": "تسجيل الدخول\\إنشاء حساب",
"Log in with Google": "تسجيل الدخول بإستخدام جوجل",
"User ID": "إسم المستخدم",
"Password": "الرقم السرى",
"Time (h:mm:ss):": "(يجب ان يكتب مثل هذا التنسيق) الوقت (h(ساعات):mm(دقائق):ss(ثوانى)):",
"Text CAPTCHA": "CAPTCHA كلامية",
"Image CAPTCHA": "CAPTCHA صورية",
"Log in/register": "تسجيل الدخول \\ إنشاء حساب",
"Log in with Google": "تسجيل الدخول باستخدام جوجل",
"User ID": "مُعرِّف المُستخدم",
"Password": "كلمة المرور",
"Time (h:mm:ss):": "الوقت (h:mm:ss):",
"Text CAPTCHA": "نص الكابتشا",
"Image CAPTCHA": "صورة الكابتشا",
"Sign In": "تسجيل الدخول",
"Register": "انشاء الحساب",
"E-mail": "الإيميل",
"Register": "التسجيل",
"E-mail": "البريد الإلكتروني",
"Google verification code": "رمز تحقق جوجل",
"Preferences": "التفضيلات",
"Player preferences": "التفضيلات المشغل",
"Always loop: ": "كرر الفيديو دائما: ",
"Autoplay: ": "تشغيل تلقائى: ",
"Play next by default: ": "شغل الفيديو التالي تلقائيا: ",
"Autoplay next video: ": "شغل الفيديو التالي تلقائيا (في قوائم التشغيل) ",
"Listen by default: ": "تشغيل النسخة السمعية تلقائى: ",
"Proxy videos: ": "عرض الفيديوهات عن طريق البروكسي؟ ",
"Player preferences": "التفضيلات المُشغِّل",
"Always loop: ": "كرر المقطع المرئيّ دائما: ",
"Autoplay: ": "تشغيل تلقائي: ",
"Play next by default: ": "شغل المقطع التالي تلقائيًا: ",
"Autoplay next video: ": "شغل المقطع التالي تلقائيًا: ",
"Listen by default: ": "تشغيل النسخة السمعية تلقائيًا: ",
"Proxy videos: ": "بروكسي المقاطع المرئيّة؟ ",
"Default speed: ": "السرعة الإفتراضية: ",
"Preferred video quality: ": "الجودة المفضلة للفيديوهات: ",
"Preferred video quality: ": "الجودة المفضلة للمقاطع: ",
"Player volume: ": "صوت المشغل: ",
"Default comments: ": "إضهار التعليقات الإفتراضية لـ: ",
"Default comments: ": "التعليقات الإفتراضية: ",
"youtube": "يوتيوب",
"reddit": "Reddit",
"Default captions: ": "الترجمات الإفتراضية: ",
"Fallback captions: ": "الترجمات المصاحبة: ",
"reddit": "ريديت",
"Default captions: ": "التسميات التوضيحية الإفتراضية: ",
"Fallback captions: ": "التسميات التوضيحية الاحتياطيَّة: ",
"Show related videos: ": "اعرض الفيديوهات ذات الصلة: ",
"Show annotations by default: ": "اعرض الملاحظات في الفيديو تلقائيا: ",
"Automatically extend video description: ": "توسيع وصف الفيديو تلقائيا: ",
"Interactive 360 degree videos: ": "مقاطع فيديو تفاعلية ب درجة 360: ",
"Visual preferences": "التفضيلات المرئية",
"Player style: ": "شكل مشغل الفيديوهات: ",
"Dark mode: ": "الوضع الليلى: ",
@ -75,11 +86,13 @@
"dark": "غامق (اسود)",
"light": "فاتح (ابيض)",
"Thin mode: ": "الوضع الخفيف: ",
"Miscellaneous preferences": "تفضيلات متنوعة",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "إعادة توجيه المثيل التلقائي (إعادة التوجيه إلى redirect.invidious.io): ",
"Subscription preferences": "تفضيلات الإشتراك",
"Show annotations by default for subscribed channels: ": "عرض الملاحظات في الفيديوهات تلقائيا في القنوات المشترك بها فقط: ",
"Redirect homepage to feed: ": "إعادة التوجية من الصفحة الرئيسية لصفحة المشتركين (لرؤية اخر فيديوهات المشتركين): ",
"Number of videos shown in feed: ": "عدد الفيديوهات التى ستظهر فى صفحة المشتركين: ",
"Sort videos by: ": "ترتيب الفيديو بـ: ",
"Sort videos by: ": "ترتيب الفيديو ب: ",
"published": "احدث فيديو",
"published - reverse": "احدث فيديو - عكسى",
"alphabetically": "ترتيب ابجدى",
@ -104,6 +117,7 @@
"Administrator preferences": "إعدادات المدير",
"Default homepage: ": "الصفحة الرئيسية الافتراضية ",
"Feed menu: ": "قائمة التدفقات: ",
"Show nickname on top: ": "إظهار اللقب في الأعلى: ",
"Top enabled: ": "تفعيل 'الأفضل' ؟ ",
"CAPTCHA enabled: ": "تفعيل الكابتشا: ",
"Login enabled: ": "تفعيل الولوج: ",
@ -113,16 +127,25 @@
"Subscription manager": "مدير الإشتراكات",
"Token manager": "إداره الرمز",
"Token": "الرمز",
"`x` subscriptions": "`x` مشتركين",
"`x` tokens": "`x` رموز",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` مشتركين",
"": "`x` مشتركين"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` رموز",
"": "`x` رموز"
},
"Import/export": "إضافة\\إستخراج",
"unsubscribe": "إلغاء الإشتراك",
"revoke": "مسح",
"Subscriptions": "الإشتراكات",
"`x` unseen notifications": "`x` إشعارات لم تشاهدها بعد",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` إشعارات لم تشاهدها بعد",
"": "`x` إشعارات لم تشاهدها بعد"
},
"search": "بحث",
"Log out": "تسجيل الخروج",
"Released under the AGPLv3 by Omar Roth.": "تم الإنشاء تحت AGPLv3 بواسطة عمر روث.",
"Released under the AGPLv3 on Github.": "تم إصداره بموجب AGPLv3 على Github.",
"Source available here.": "الأكواد متوفرة هنا.",
"View JavaScript license information.": "مشاهدة معلومات حول تراخيص الجافاسكريبت.",
"View privacy policy.": "عرض سياسة الخصوصية.",
@ -138,7 +161,11 @@
"Title": "العنوان",
"Playlist privacy": "إعدادات الخصوصيه",
"Editing playlist `x`": "تعديل قائمه التشفيل `x`",
"Show more": "أظهر المزيد",
"Show less": "عرض اقل",
"Watch on YouTube": "مشاهدة الفيديو على اليوتيوب",
"Switch Invidious Instance": "تبديل المثيل Invidious",
"Broken? Try another Invidious Instance": "معطل؟ جرب مثيل Invidious آخر",
"Hide annotations": "إخفاء الملاحظات فى الفيديو",
"Show annotations": "عرض الملاحظات فى الفيديو",
"Genre: ": "النوع: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "الدول المسموح فيها هذا الفيديو: ",
"Blacklisted regions: ": "الدول الحظور فيها هذا الفيديو: ",
"Shared `x`": "شارك منذ `x`",
"`x` views": "`x` مشاهدات",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` مشاهدات",
"": "`x` مشاهدات"
},
"Premieres in `x`": "يعرض فى `x`",
"Premieres `x`": "يعرض `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.": "اهلا! يبدو ان الجافاسكريبت معطلة. اضغط هنا لعرض التعليقات, ضع فى إعتبارك انها ستأخذ وقت اطول للعرض.",
"View YouTube comments": "عرض تعليقات اليوتيوب",
"View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع Reddit",
"View `x` comments": "عرض `x` تعليقات",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليقات",
"": "عرض `x` تعليقات"
},
"View Reddit comments": "عرض تعليقات ريدإت Reddit",
"Hide replies": "إخفاء الردود",
"Show replies": "عرض الردود",
@ -174,16 +207,22 @@
"Password cannot be empty": "الرقم السرى لايمكن ان يكون فارغ",
"Password cannot be longer than 55 characters": "الرقم السرى لا يتعدى 55 حرف",
"Please log in": "الرجاء تسجيل الدخول",
"Invidious Private Feed for `x`": "صفحة Invidious للمشتركين الخاصة\\مخفية لـ `x`",
"Invidious Private Feed for `x`": "تغذية Invidious خاصة ل 'x'",
"channel:`x`": "قناة:`x`",
"Deleted or invalid channel": "قناة ممسوحة او غير صالحة",
"This channel does not exist.": "القناة غير موجودة.",
"Could not get channel info.": "لم يستطع الحصول على معلومات القناة.",
"Could not fetch comments": "لم يتمكن من إحضار التعليقات",
"View `x` replies": "عرض `x` ردود",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` ردود",
"": "عرض `x` ردود"
},
"`x` ago": "`x` منذ",
"Load more": "عرض المزيد",
"`x` points": "`x` نقاط",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` نقاط",
"": "`x` نقاط"
},
"Could not create mix.": "لم يستطع عمل خلط.",
"Empty playlist": "قائمة التشغيل فارغة",
"Not a playlist.": "قائمة التشغيل غير صالحة.",
@ -301,15 +340,37 @@
"Yiddish": "اليديشية",
"Yoruba": "اليوروبا",
"Zulu": "الزولو",
"`x` years": "`x` سنوات",
"`x` months": "`x` شهور",
"`x` weeks": "`x` اسابيع",
"`x` days": "`x` ايام",
"`x` hours": "`x` ساعات",
"`x` minutes": "`x` دقائق",
"`x` seconds": "`x` ثوانى",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` سنوات",
"": "`x` سنوات"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` شهور",
"": "`x` شهور"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` اسابيع",
"": "`x` اسابيع"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ايام",
"": "`x` ايام"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ساعات",
"": "`x` ساعات"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` دقائق",
"": "`x` دقائق"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ثوانى",
"": "`x` ثوانى"
},
"Fallback comments: ": "التعليقات البديلة: ",
"Popular": "الأكثر شعبية",
"Search": "بحث",
"Top": "الأفضل",
"About": "حول",
"Rating: ": "التقييم: ",
@ -321,7 +382,7 @@
"News": "الأخبار",
"Movies": "الأفلام",
"Download": "نزّل",
"Download as: ": "نزّله كـ: ",
"Download as: ": "نزله كـ:. ",
"%A %B %-d, %Y": "%A %-d %B %Y",
"(edited)": "(تم تعديلة)",
"YouTube comment permalink": "رابط التعليق على اليوتيوب",
@ -332,5 +393,35 @@
"Videos": "الفيديوهات",
"Playlists": "قوائم التشغيل",
"Community": "المجتمع",
"Current version: ": "الإصدار الحالي: "
}
"relevance": "ملاءم",
"rating": "تقييم",
"date": "التاريخ",
"views": "مشاهدات",
"content_type": "نوع المحتوى",
"duration": "المدة الزمنية",
"features": "الميزات",
"sort": "فرز",
"hour": "ساعة",
"today": "اليوم",
"week": "إسبوع",
"month": "شهر",
"year": "سنة",
"video": "فيديو",
"channel": "قناة",
"playlist": "قائمة التشغيل",
"movie": "فيلم",
"show": "عرض",
"hd": "عالية الدقة",
"subtitles": "ترجمات",
"creative_commons": "المشاع الإبداعي",
"3d": "ثلاثي الأبعاد",
"live": "مباشر",
"4k": "4k",
"location": "الاماكن",
"hdr": "وضع التباين العالي",
"filter": "معامل الفرز",
"Current version: ": "الإصدار الحالي: ",
"next_steps_error_message": "بعد ذلك يجب أن تحاول: ",
"next_steps_error_message_refresh": "تحديث",
"next_steps_error_message_go_to_youtube": "انتقل إلى يوتيوب"
}

View File

@ -1,10 +1,16 @@
{
"`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` সাবস্ক্রাইবার।([^.,0-9]|^)1([^.,0-9]|$)",
"`x` subscribers.": "`x` সাবস্ক্রাইবার।",
"`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ভিডিও।([^.,0-9]|^)1([^.,0-9]|$)",
"`x` videos.": "`x` ভিডিও।",
"`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "`x` প্লেলিস্ট।[^.,0-9]|^)1([^.,0-9]|$)",
"`x` playlists.": "`x` প্লেলিস্ট।",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` সাবস্ক্রাইবার",
"": "`x` সাবস্ক্রাইবার"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ভিডিও",
"": "`x` ভিডিও"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` প্লেলিস্ট",
"": "`x` প্লেলিস্ট"
},
"LIVE": "লাইভ",
"Shared `x` ago": "`x` আগে শেয়ার করা হয়েছে",
"Unsubscribe": "আনসাবস্ক্রাইব",
@ -59,11 +65,11 @@
"Autoplay: ": "স্বয়ংক্রিয় চালু: ",
"Play next by default: ": "ডিফল্টভাবে পরবর্তী চালাও: ",
"Autoplay next video: ": "পরবর্তী ভিডিও স্বয়ংক্রিয়ভাবে চালাও: ",
"Listen by default: ": "",
"Proxy videos: ": "",
"Default speed: ": "",
"Preferred video quality: ": "",
"Player volume: ": "",
"Listen by default: ": "সহজাতভাবে শোনো: ",
"Proxy videos: ": "ভিডিও প্রক্সি করো: ",
"Default speed: ": "সহজাত গতি: ",
"Preferred video quality: ": "পছন্দের ভিডিও মান: ",
"Player volume: ": "প্লেয়ার শব্দের মাত্রা: ",
"Default comments: ": "",
"youtube": "",
"reddit": "",
@ -71,6 +77,8 @@
"Fallback captions: ": "",
"Show related videos: ": "",
"Show annotations by default: ": "",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "",
"Player style: ": "",
"Dark mode: ": "",
@ -78,6 +86,8 @@
"dark": "",
"light": "",
"Thin mode: ": "",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "",
"Show annotations by default for subscribed channels: ": "",
"Redirect homepage to feed: ": "",
@ -107,6 +117,7 @@
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Show nickname on top: ": "",
"Top enabled: ": "",
"CAPTCHA enabled: ": "",
"Login enabled: ": "",
@ -116,19 +127,25 @@
"Subscription manager": "",
"Token manager": "",
"Token": "",
"`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` subscriptions.": "",
"`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` tokens.": "",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Import/export": "",
"unsubscribe": "",
"revoke": "",
"Subscriptions": "",
"`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` unseen notifications.": "",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"search": "",
"Log out": "",
"Released under the AGPLv3 by Omar Roth.": "",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "",
"View JavaScript license information.": "",
"View privacy policy.": "",
@ -144,7 +161,11 @@
"Title": "",
"Playlist privacy": "",
"Editing playlist `x`": "",
"Show more": "",
"Show less": "",
"Watch on YouTube": "",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "",
"Show annotations": "",
"Genre: ": "",
@ -155,15 +176,19 @@
"Whitelisted regions: ": "",
"Blacklisted regions: ": "",
"Shared `x`": "",
"`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` views.": "",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Premieres in `x`": "",
"Premieres `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.": "",
"View YouTube comments": "",
"View more comments on Reddit": "",
"View `x` comments.([^.,0-9]|^)1([^.,0-9]|$)": "",
"View `x` comments.": "",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"View Reddit comments": "",
"Hide replies": "",
"Show replies": "",
@ -188,12 +213,16 @@
"This channel does not exist.": "",
"Could not get channel info.": "",
"Could not fetch comments": "",
"View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "",
"View `x` replies.": "",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` ago": "",
"Load more": "",
"`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` points.": "",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Could not create mix.": "",
"Empty playlist": "",
"Not a playlist.": "",
@ -311,22 +340,37 @@
"Yiddish": "",
"Yoruba": "",
"Zulu": "",
"`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` years.": "",
"`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` months.": "",
"`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` weeks.": "",
"`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` days.": "",
"`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` hours.": "",
"`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` minutes.": "",
"`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` seconds.": "",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Fallback comments: ": "",
"Popular": "",
"Search": "",
"Top": "",
"About": "",
"Rating: ": "",
@ -349,5 +393,35 @@
"Videos": "",
"Playlists": "",
"Community": "",
"Current version: ": ""
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,22 +1,22 @@
{
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` odběratelů.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` odběratelů."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` odběratelů",
"": "`x` odběratelů"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videí.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` videí."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videí",
"": "`x` videí"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` playlist",
"": "`x` playlisty"
},
"LIVE": "ŽIVĚ",
"Shared `x` ago": "",
"Shared `x` ago": "Sdíleno před `x`",
"Unsubscribe": "Odhlásit odběr",
"Subscribe": "Odebírat",
"View channel on YouTube": "Otevřít kanál na YouTube",
"View playlist on YouTube": "",
"View playlist on YouTube": "Zobrazit playlist na YouTube",
"newest": "nejnovější",
"oldest": "nejstarší",
"popular": "populární",
@ -28,7 +28,7 @@
"New passwords must match": "Hesla se musí schodovat",
"Cannot change password for Google accounts": "Nelze změnit heslo pro účty Google",
"Authorize token?": "Autorizovat token?",
"Authorize token for `x`?": "",
"Authorize token for `x`?": "Autorizovat token pro `x`?",
"Yes": "Ano",
"No": "Ne",
"Import and Export Data": "Import a Export údajů",
@ -63,107 +63,116 @@
"Player preferences": "Nastavení přehravače",
"Always loop: ": "Vždy opakovat: ",
"Autoplay: ": "Automatické přehrávání: ",
"Play next by default: ": "",
"Autoplay next video: ": "",
"Listen by default: ": "",
"Proxy videos: ": "",
"Default speed: ": "",
"Preferred video quality: ": "",
"Play next by default: ": "Přehrát další ve výchozím stavu: ",
"Autoplay next video: ": "Automaticky přehrát další video: ",
"Listen by default: ": "Poslouchat ve výchozím nastavení: ",
"Proxy videos: ": "Video přes proxy: ",
"Default speed: ": "Základní Rychlost: ",
"Preferred video quality: ": "Preferovaná kvalita videa: ",
"Player volume: ": "Hlasitost přehrávače: ",
"Default comments: ": "",
"youtube": "youtube",
"Default comments: ": "Předpřipravené komentáře: ",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "",
"Fallback captions: ": "",
"Default captions: ": "Standartní Titulky: ",
"Fallback captions: ": "Záložní titulky: ",
"Show related videos: ": "Zobrazit podobné videa: ",
"Show annotations by default: ": "",
"Visual preferences": "",
"Show annotations by default: ": "Zobrazovat poznámky ve výchozím nastavení: ",
"Automatically extend video description: ": "Rozšířit automaticky popis u videa: ",
"Interactive 360 degree videos: ": "",
"Visual preferences": "Nastavení vzhledu",
"Player style: ": "Styl přehrávače ",
"Dark mode: ": "Tmavý režim ",
"Theme: ": "",
"dark": "",
"light": "",
"Thin mode: ": "",
"Subscription preferences": "",
"Show annotations by default for subscribed channels: ": "",
"Redirect homepage to feed: ": "",
"Number of videos shown in feed: ": "",
"Theme: ": "Vzhled: ",
"dark": "tmavý",
"light": "světlý",
"Thin mode: ": "Kompaktní režim: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Nastavení předplatných",
"Show annotations by default for subscribed channels: ": "Ve výchozím nastavení zobrazovat poznámky u odebíraných kanálů: ",
"Redirect homepage to feed: ": "Přesměrovávat domovskou stránku na informační kanál: ",
"Number of videos shown in feed: ": "Počet videí zobrazovaných v informačním kanále: ",
"Sort videos by: ": "Roztřídit videa podle: ",
"published": "publikováno",
"published - reverse": "",
"published - reverse": "podle publikování - obrátit",
"alphabetically": "podle abecedy",
"alphabetically - reverse": "",
"alphabetically - reverse": "podle abecedy - převrátit",
"channel name": "název kanálu",
"channel name - reverse": "",
"channel name - reverse": "podle jména kanálu - převrátit",
"Only show latest video from channel: ": "Jenom zobrazit nejnovjejší video z kanálu: ",
"Only show latest unwatched video from channel: ": "",
"Only show unwatched: ": "",
"Only show notifications (if there are any): ": "",
"Only show latest unwatched video from channel: ": "Zobrazit jen nejnovější nezhlédnuté video z daného kanálu: ",
"Only show unwatched: ": "Zobrazit jen již nezhlédnuté: ",
"Only show notifications (if there are any): ": "Zobrazit pouze upozornění (pokud nějaká jsou): ",
"Enable web notifications": "Povolit webové upozornění",
"`x` uploaded a video": "`x` nahrál(a) video",
"`x` is live": "`x` je živě",
"Data preferences": "",
"Data preferences": "Nastavení dat",
"Clear watch history": "Smazat historii",
"Import/export data": "",
"Import/export data": "importovat/exportovat data",
"Change password": "Změnit heslo",
"Manage subscriptions": "",
"Manage tokens": "",
"Watch history": "",
"Delete account": "",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Manage subscriptions": "Spravovat odebírané kanály",
"Manage tokens": "Spravovat klíče",
"Watch history": "Historie Sledování",
"Delete account": "Smazat Účet",
"Administrator preferences": "Administrátorská nastavení",
"Default homepage: ": "Základní domovská stránka: ",
"Feed menu: ": "Menu doporučených: ",
"Show nickname on top: ": "",
"Top enabled: ": "",
"CAPTCHA enabled: ": "",
"Login enabled: ": "",
"Registration enabled: ": "",
"Report statistics: ": "",
"Save preferences": "",
"Subscription manager": "",
"Token manager": "",
"Token": "",
"CAPTCHA enabled: ": "CAPTCHA povolen: ",
"Login enabled: ": "Přihlášení povoleno: ",
"Registration enabled: ": "Registrace povolena ",
"Report statistics: ": "Oznámit statistiky: ",
"Save preferences": "Uložit nastavení",
"Subscription manager": "Správa Odběrů",
"Token manager": "Správa klíčů",
"Token": "Klíč",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Odběry",
"": "`x` Odebíraných kanálů"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Klíčů",
"": "`x` klíčů"
},
"Import/export": "",
"unsubscribe": "",
"revoke": "",
"Subscriptions": "",
"Import/export": "Importovat/exportovat",
"unsubscribe": "odhlásit odběr",
"revoke": "vrátit zpět",
"Subscriptions": "Odběry",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` nezhlédnutých oznámení",
"": "`x` nezhlédnutých oznámení"
},
"search": "",
"Log out": "",
"Released under the AGPLv3 by Omar Roth.": "",
"Source available here.": "",
"View JavaScript license information.": "",
"View privacy policy.": "",
"Trending": "",
"Public": "",
"Unlisted": "",
"Private": "",
"View all playlists": "",
"Updated `x` ago": "",
"Delete playlist `x`?": "",
"Delete playlist": "",
"Create playlist": "",
"Title": "",
"search": "hledat",
"Log out": "Odhlásit se",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Zdrojový kód dostupný zde.",
"View JavaScript license information.": "Zobrazit informace o licenci JavaScript .",
"View privacy policy.": "Zobrazit Zásady ochrany osobních údajů.",
"Trending": "Trendy",
"Public": "Veřejné",
"Unlisted": "Nevypsáno",
"Private": "Soukromé",
"View all playlists": "Zobrazit všechny playlisty",
"Updated `x` ago": "Aktualizováno před `x`",
"Delete playlist `x`?": "Smazat playlist `x`?",
"Delete playlist": "Smazat playlist",
"Create playlist": "Vytvořit playlist",
"Title": "Název",
"Playlist privacy": "",
"Editing playlist `x`": "",
"Watch on YouTube": "",
"Hide annotations": "",
"Show annotations": "",
"Genre: ": "",
"License: ": "",
"Family friendly? ": "",
"Editing playlist `x`": "Upravování playlistu `x`",
"Show more": "Zobrazit více",
"Show less": "Zobrazit méně",
"Watch on YouTube": "Sledovat na YouTube",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "Skrýt vysvětlivky",
"Show annotations": "Zobrazit vysvětlivky",
"Genre: ": "Žánr: ",
"License: ": "Licence: ",
"Family friendly? ": "Vhodné pro děti? ",
"Wilson score: ": "",
"Engagement: ": "",
"Engagement: ": "Závaznost: ",
"Whitelisted regions: ": "",
"Blacklisted regions: ": "",
"Shared `x`": "",
@ -225,112 +234,112 @@
"Erroneous token": "",
"No such user": "",
"Token is expired, please try again": "",
"English": "",
"English (auto-generated)": "",
"Afrikaans": "",
"Albanian": "",
"Amharic": "",
"Arabic": "",
"Armenian": "",
"Azerbaijani": "",
"Bangla": "",
"Basque": "",
"Belarusian": "",
"Bosnian": "",
"Bulgarian": "",
"Burmese": "",
"Catalan": "",
"Cebuano": "",
"Chinese (Simplified)": "",
"Chinese (Traditional)": "",
"Corsican": "",
"Croatian": "",
"Czech": "",
"Danish": "",
"Dutch": "",
"Esperanto": "",
"Estonian": "",
"Filipino": "",
"Finnish": "",
"French": "",
"Galician": "",
"Georgian": "",
"German": "",
"Greek": "",
"Gujarati": "",
"Haitian Creole": "",
"Hausa": "",
"Hawaiian": "",
"Hebrew": "",
"Hindi": "",
"Hmong": "",
"Hungarian": "",
"Icelandic": "",
"Igbo": "",
"Indonesian": "",
"Irish": "",
"Italian": "",
"Japanese": "",
"Javanese": "",
"Kannada": "",
"Kazakh": "",
"Khmer": "",
"Korean": "",
"Kurdish": "",
"Kyrgyz": "",
"Lao": "",
"Latin": "",
"Latvian": "",
"Lithuanian": "",
"Luxembourgish": "",
"Macedonian": "",
"Malagasy": "",
"Malay": "",
"Malayalam": "",
"Maltese": "",
"Maori": "",
"Marathi": "",
"Mongolian": "",
"Nepali": "",
"Norwegian Bokmål": "",
"Nyanja": "",
"Pashto": "",
"Persian": "",
"Polish": "",
"Portuguese": "",
"Punjabi": "",
"Romanian": "",
"Russian": "",
"Samoan": "",
"Scottish Gaelic": "",
"Serbian": "",
"Shona": "",
"Sindhi": "",
"Sinhala": "",
"Slovak": "",
"Slovenian": "",
"Somali": "",
"Southern Sotho": "",
"Spanish": "",
"Spanish (Latin America)": "",
"Sundanese": "",
"Swahili": "",
"Swedish": "",
"Tajik": "",
"Tamil": "",
"Telugu": "",
"Thai": "",
"Turkish": "",
"Ukrainian": "",
"Urdu": "",
"Uzbek": "",
"Vietnamese": "",
"Welsh": "",
"Western Frisian": "",
"Xhosa": "",
"Yiddish": "",
"Yoruba": "",
"Zulu": "",
"English": "Angličtina",
"English (auto-generated)": "Angličtina (automaticky generováno)",
"Afrikaans": "Afrikánština",
"Albanian": "Albánština",
"Amharic": "Amharština",
"Arabic": "Arabština",
"Armenian": "Arménština",
"Azerbaijani": "Azerbajdžánština",
"Bangla": "Bengálština",
"Basque": "Baskičtina",
"Belarusian": "Běloruština",
"Bosnian": "Bosenština",
"Bulgarian": "Bulharština",
"Burmese": "Barmština",
"Catalan": "Katalánština",
"Cebuano": "Cebuánština",
"Chinese (Simplified)": "Čínština (zjednodušená)",
"Chinese (Traditional)": "Čínština (tradiční)",
"Corsican": "Korsičtina",
"Croatian": "Chorvatština",
"Czech": "Čeština",
"Danish": "Dánština",
"Dutch": "Nizozemština",
"Esperanto": "Esperanto",
"Estonian": "Estonština",
"Filipino": "Filipínština",
"Finnish": "Finština",
"French": "Francouzština",
"Galician": "Galicijština",
"Georgian": "Gruzínština",
"German": "Němčina",
"Greek": "Řečtina",
"Gujarati": "Gudžarátština",
"Haitian Creole": "Haitská kreolština",
"Hausa": "Hauština",
"Hawaiian": "Havajština",
"Hebrew": "Hebrejština",
"Hindi": "Hindština",
"Hmong": "Hmongština",
"Hungarian": "Maďarština",
"Icelandic": "Islandština",
"Igbo": "Igboština",
"Indonesian": "Indonéština",
"Irish": "Irština",
"Italian": "Italština",
"Japanese": "Japonština",
"Javanese": "Javánština",
"Kannada": "Kannadština",
"Kazakh": "Kazaština",
"Khmer": "Khmerština",
"Korean": "Korejština",
"Kurdish": "Kurdština",
"Kyrgyz": "Kyrgyzština",
"Lao": "Laoština",
"Latin": "Latina",
"Latvian": "Lotyština",
"Lithuanian": "Litevština",
"Luxembourgish": "Lucemburština",
"Macedonian": "Makedonština",
"Malagasy": "Malgaština",
"Malay": "Malajština",
"Malayalam": "Malajálamština",
"Maltese": "Maltština",
"Maori": "Maorština",
"Marathi": "Maráthština",
"Mongolian": "Mongolština",
"Nepali": "Nepálština",
"Norwegian Bokmål": "Norština Bokmål",
"Nyanja": "Čičevština",
"Pashto": "Paštština",
"Persian": "Perština",
"Polish": "Polština",
"Portuguese": "Portugalština",
"Punjabi": "Paňdžábština",
"Romanian": "Rumunština",
"Russian": "Ruština",
"Samoan": "Samojština",
"Scottish Gaelic": "Skotská gaelština",
"Serbian": "Srbština",
"Shona": "Shona",
"Sindhi": "Sindhština",
"Sinhala": "Sinhálština",
"Slovak": "Slovenština",
"Slovenian": "Slovinština",
"Somali": "Somálština",
"Southern Sotho": "Sesothština",
"Spanish": "Španělština",
"Spanish (Latin America)": "Španělština (Latinská Amerika)",
"Sundanese": "Sundština",
"Swahili": "Svahilština",
"Swedish": "Švédština",
"Tajik": "Tádžičtina",
"Tamil": "Tamilština",
"Telugu": "Telugština",
"Thai": "Thajština",
"Turkish": "Turečtina",
"Ukrainian": "Ukrajinština",
"Urdu": "Urdština",
"Uzbek": "Uzbečtina",
"Vietnamese": "Vietnamština",
"Welsh": "Velština",
"Western Frisian": "Západofríština",
"Xhosa": "Xhoština",
"Yiddish": "Jidiš",
"Yoruba": "Jorubština",
"Zulu": "Zuluština",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
@ -360,17 +369,18 @@
"": ""
},
"Fallback comments: ": "",
"Popular": "",
"Popular": "Populární",
"Search": "",
"Top": "",
"About": "Informace",
"Rating: ": "Hodnocení: ",
"Language: ": "Jazyk: ",
"View as playlist": "",
"Default": "",
"Default": "Výchozí",
"Music": "Hudba",
"Gaming": "",
"Gaming": "Hry",
"News": "Zprávy",
"Movies": "",
"Movies": "Filmy",
"Download": "Stáhnout",
"Download as: ": "Stáhnout jako: ",
"%A %B %-d, %Y": "",
@ -398,17 +408,20 @@
"year": "rok",
"video": "video",
"channel": "kanál",
"playlist": "",
"movie": "",
"playlist": "playlist",
"movie": "film",
"show": "zobrazit",
"hd": "HD",
"subtitles": "titulky",
"creative_commons": "",
"creative_commons": "Creative Commons",
"3d": "3D",
"live": "živě",
"4k": "4k",
"location": "umístění",
"hdr": "HDR",
"filter": "filtr",
"Current version: ": ""
"Current version: ": "",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,19 +1,19 @@
{
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnenter.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` abonnenter."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnenter",
"": "`x` abonnenter"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videoer.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` videoer."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videoer",
"": "`x` videoer"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` afspilningslister.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` afspilningslister."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` afspilningslister",
"": "`x` afspilningslister"
},
"LIVE": "DIREKTE",
"Shared `x` ago": "Delt for `x` siden",
"Unsubscribe": "",
"Unsubscribe": "Opsig abonnement",
"Subscribe": "Abonner",
"View channel on YouTube": "Vis kanal på YouTube",
"View playlist on YouTube": "Vis afspilningsliste på YouTube",
@ -28,13 +28,13 @@
"New passwords must match": "Nye kodeord skal matche",
"Cannot change password for Google accounts": "Kan ikke skifte kodeord til Google-konti",
"Authorize token?": "Godkend token?",
"Authorize token for `x`?": "Godkende token til `x`?",
"Authorize token for `x`?": "Godkend token til `x`?",
"Yes": "Ja",
"No": "Nej",
"Import and Export Data": "Importer og Eksporter Data",
"Import": "Importer",
"Import Invidious data": "Importer Invidious data",
"Import YouTube subscriptions": "Importer Youtube abonnementer",
"Import YouTube subscriptions": "Importer YouTube abonnementer",
"Import FreeTube subscriptions (.db)": "Importer FreeTube abonnementer (.db)",
"Import NewPipe subscriptions (.json)": "Importer NewPipe abonnementer (.json)",
"Import NewPipe data (.zip)": "Importer NewPipe data (.zip)",
@ -44,7 +44,7 @@
"Export data as JSON": "Exporter data som JSON",
"Delete account?": "Slet konto?",
"History": "Historik",
"An alternative front-end to YouTube": "",
"An alternative front-end to YouTube": "En alternativ forside til YouTube",
"JavaScript license information": "JavaScript licens information",
"source": "kilde",
"Log in": "Log på",
@ -58,9 +58,9 @@
"Sign In": "Log ind",
"Register": "Registrer",
"E-mail": "E-mail",
"Google verification code": "Google verifications kode",
"Google verification code": "Google-verifikationskode",
"Preferences": "Præferencer",
"Player preferences": "",
"Player preferences": "Afspillerindstillinger",
"Always loop: ": "Altid gentag: ",
"Autoplay: ": "Auto afspil: ",
"Play next by default: ": "Afspil næste som standard: ",
@ -71,153 +71,162 @@
"Preferred video quality: ": "Foretrukken video kvalitet: ",
"Player volume: ": "Lydstyrke: ",
"Default comments: ": "Standard kommentarer: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "",
"Fallback captions: ": "",
"Show related videos: ": "",
"Show annotations by default: ": "",
"Visual preferences": "",
"Player style: ": "",
"Dark mode: ": "",
"Theme: ": "",
"dark": "",
"light": "",
"Thin mode: ": "",
"Subscription preferences": "",
"Show annotations by default for subscribed channels: ": "",
"Redirect homepage to feed: ": "",
"Number of videos shown in feed: ": "",
"Sort videos by: ": "",
"published": "",
"published - reverse": "",
"alphabetically": "",
"alphabetically - reverse": "",
"channel name": "",
"channel name - reverse": "",
"Only show latest video from channel: ": "",
"Only show latest unwatched video from channel: ": "",
"Only show unwatched: ": "",
"Only show notifications (if there are any): ": "",
"Enable web notifications": "",
"`x` uploaded a video": "",
"`x` is live": "",
"Data preferences": "",
"Clear watch history": "",
"Import/export data": "",
"Change password": "",
"Manage subscriptions": "",
"Manage tokens": "",
"Watch history": "",
"Delete account": "",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Top enabled: ": "",
"CAPTCHA enabled: ": "",
"Login enabled: ": "",
"Registration enabled: ": "",
"Report statistics: ": "",
"Save preferences": "",
"Subscription manager": "",
"Token manager": "",
"Token": "",
"Default captions: ": "Standard undertekster: ",
"Fallback captions: ": "Alternative undertekster: ",
"Show related videos: ": "Vis relaterede videoer: ",
"Show annotations by default: ": "Vis annotationer som standard: ",
"Automatically extend video description: ": "Automatisk udvid videoens beskrivelse: ",
"Interactive 360 degree videos: ": "Interaktiv 360 graders videoer: ",
"Visual preferences": "Visuelle præferencer",
"Player style: ": "Afspiller stil: ",
"Dark mode: ": "Mørk tilstand: ",
"Theme: ": "Tema: ",
"dark": "mørk",
"light": "lys",
"Thin mode: ": "Tynd tilstand: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Abonnements præferencer",
"Show annotations by default for subscribed channels: ": "Vis annotationer som standard for abonnerede kanaler: ",
"Redirect homepage to feed: ": "Omdiriger startside til feed: ",
"Number of videos shown in feed: ": "Antal videoer vist i feed: ",
"Sort videos by: ": "Sorter videoer efter: ",
"published": "offentliggjort",
"published - reverse": "offentliggjort - omvendt",
"alphabetically": "alfabetisk",
"alphabetically - reverse": "alfabetisk - omvendt",
"channel name": "kanalnavn",
"channel name - reverse": "kanalnavn - omvendt",
"Only show latest video from channel: ": "Vis kun seneste video fra kanal: ",
"Only show latest unwatched video from channel: ": "Vis kun seneste usete video fra kanal: ",
"Only show unwatched: ": "Vis kun usete: ",
"Only show notifications (if there are any): ": "Vis kun notifikationer (hvis der er nogle): ",
"Enable web notifications": "Aktiver webnotifikationer",
"`x` uploaded a video": "`x` uploadede en video",
"`x` is live": "`x` er live",
"Data preferences": "Data præferencer",
"Clear watch history": "Ryd afspilningshistorik",
"Import/export data": "Importer/exporter data",
"Change password": "Skift adgangskode",
"Manage subscriptions": "Administrer abonnementer",
"Manage tokens": "Administrer tokens",
"Watch history": "Afspilningshistorik",
"Delete account": "Slet konto",
"Administrator preferences": "Administrator præferencer",
"Default homepage: ": "Standard startside: ",
"Feed menu: ": "Feed menu: ",
"Show nickname on top: ": "",
"Top enabled: ": "Top aktiveret: ",
"CAPTCHA enabled: ": "CAPTCHA aktiveret: ",
"Login enabled: ": "Login aktiveret: ",
"Registration enabled: ": "Registrering aktiveret: ",
"Report statistics: ": "Indsend statistik: ",
"Save preferences": "Gem præferencer",
"Subscription manager": "Abonnementsmanager",
"Token manager": "Tokenmanager",
"Token": "Token",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnementer",
"": "`x`"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens",
"": "`x` tokens"
},
"Import/export": "",
"unsubscribe": "",
"revoke": "",
"Subscriptions": "",
"Import/export": "Importer/eksporter",
"unsubscribe": "opsig abonnement",
"revoke": "tilbagekald",
"Subscriptions": "Abonnementer",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` usete notifikationer",
"": "`x` usete notifikationer"
},
"search": "",
"Log out": "",
"Released under the AGPLv3 by Omar Roth.": "",
"Source available here.": "",
"View JavaScript license information.": "",
"View privacy policy.": "",
"Trending": "",
"Public": "",
"Unlisted": "",
"Private": "",
"View all playlists": "",
"Updated `x` ago": "",
"Delete playlist `x`?": "",
"Delete playlist": "",
"Create playlist": "",
"Title": "",
"Playlist privacy": "",
"Editing playlist `x`": "",
"Watch on YouTube": "",
"Hide annotations": "",
"Show annotations": "",
"Genre: ": "",
"License: ": "",
"Family friendly? ": "",
"Wilson score: ": "",
"Engagement: ": "",
"Whitelisted regions: ": "",
"Blacklisted regions: ": "",
"Shared `x`": "",
"search": "søg",
"Log out": "Log ud",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Kilde tilgængelig her.",
"View JavaScript license information.": "Vis JavaScriptlicensinformation.",
"View privacy policy.": "Vis privatpolitik.",
"Trending": "Trending",
"Public": "Offentlig",
"Unlisted": "Skjult",
"Private": "Privat",
"View all playlists": "Vis alle afspilningslister",
"Updated `x` ago": "Opdateret for 'x' siden",
"Delete playlist `x`?": "Opdateret `x` siden",
"Delete playlist": "Slet afspilningsliste",
"Create playlist": "Opret afspilningsliste",
"Title": "Titel",
"Playlist privacy": "Privatlivsindstillinger for afspilningsliste",
"Editing playlist `x`": "Redigerer afspilningsliste `x`",
"Show more": "Vis mere",
"Show less": "Vis mindre",
"Watch on YouTube": "Se på YouTube",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "Skjul annotationer",
"Show annotations": "Vis annotationer",
"Genre: ": "Genre: ",
"License: ": "Licens: ",
"Family friendly? ": "Familievenlig? ",
"Wilson score: ": "Wilson score: ",
"Engagement: ": "Engagement: ",
"Whitelisted regions: ": "Whitelistede regioner: ",
"Blacklisted regions: ": "Blacklistede regioner: ",
"Shared `x`": "Delt `x`",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` visninger.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` visninger"
},
"Premieres in `x`": "",
"Premieres `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.": "",
"View YouTube comments": "",
"View more comments on Reddit": "",
"Premieres in `x`": "Har premiere om `x`",
"Premieres `x`": "Har premiere om `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.": "Hej! Det ser ud til at du har JavaScript slået fra. Klik her for at se kommentarer, vær opmærksom på at de kan tage længere om at loade.",
"View YouTube comments": "Vis YouTube kommentarer",
"View more comments on Reddit": "Se flere kommentarer på Reddit",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` kommentarer.([^.,0-9]|^)1([^.,0-9]|$)",
"": "Vis `x` kommentarer"
},
"View Reddit comments": "",
"Hide replies": "",
"Show replies": "",
"Incorrect password": "",
"Quota exceeded, try again in a few hours": "",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "",
"Invalid TFA code": "",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "",
"Wrong answer": "",
"Erroneous CAPTCHA": "",
"CAPTCHA is a required field": "",
"User ID is a required field": "",
"Password is a required field": "",
"Wrong username or password": "",
"Please sign in using 'Log in with Google'": "",
"Password cannot be empty": "",
"Password cannot be longer than 55 characters": "",
"Please log in": "",
"View Reddit comments": "Vis Reddit kommentarer",
"Hide replies": "Skjul svar",
"Show replies": "Vis svar",
"Incorrect password": "Forkert adgangskode",
"Quota exceeded, try again in a few hours": "Kvota overskredet, prøv igen om et par timer",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Login fejlet, tjek at totrinsbekræftelse (Authenticator eller SMS) er slået til.",
"Invalid TFA code": "Ugyldig TFA kode",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Login fejlede. Det er måske på grund af to-faktor-autentisering ikk er slået til for din konto.",
"Wrong answer": "Forkert svar",
"Erroneous CAPTCHA": "Fejlagtig CAPTCHA",
"CAPTCHA is a required field": "CAPTCHA er et krævet felt",
"User ID is a required field": "Bruger ID er et krævet felt",
"Password is a required field": "Adgangskode er et krævet felt",
"Wrong username or password": "Forkert brugernavn eller adgangskode",
"Please sign in using 'Log in with Google'": "Venligst tjek in via 'Log in med Google'",
"Password cannot be empty": "Adgangskode kan ikke være tom",
"Password cannot be longer than 55 characters": "Adgangskoden må ikke være længere end 55 tegn",
"Please log in": "Venligst log in",
"Invidious Private Feed for `x`": "",
"channel:`x`": "",
"Deleted or invalid channel": "",
"This channel does not exist.": "",
"Could not get channel info.": "",
"Could not fetch comments": "",
"channel:`x`": "kanal: 'x'",
"Deleted or invalid channel": "Slettet eller invalid kanal",
"This channel does not exist.": "Denne kanal eksisterer ikke.",
"Could not get channel info.": "Kunne ikke hente kanal info.",
"Could not fetch comments": "Kunne ikke hente kommentarer",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` besvarelser",
"": "Vis 'x' besvarelser"
},
"`x` ago": "",
"Load more": "",
"`x` ago": "'x' siden",
"Load more": "Hent flere",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` point",
"": "'x' point"
},
"Could not create mix.": "",
"Empty playlist": "",
"Not a playlist.": "",
"Playlist does not exist.": "",
"Could not create mix.": "Kunne ikke skabe blanding.",
"Empty playlist": "Tom playliste",
"Not a playlist.": "Ikke en playliste.",
"Playlist does not exist.": "Playlist eksisterer ikke.",
"Could not pull trending pages.": "",
"Hidden field \"challenge\" is a required field": "",
"Hidden field \"token\" is a required field": "",
@ -361,6 +370,7 @@
},
"Fallback comments: ": "",
"Popular": "",
"Search": "",
"Top": "",
"About": "",
"Rating: ": "",
@ -383,5 +393,35 @@
"Videos": "",
"Playlists": "",
"Community": "",
"Current version: ": ""
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,10 +1,19 @@
{
"`x` subscribers": "`x` Abonnenten",
"`x` videos": "`x` Videos",
"`x` playlists": "`x` Wiedergabelisten",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Abonnenten",
"": "`x` Abonnenten"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Videos",
"": "`x` Videos"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Wiedergabelisten",
"": "`x` Wiedergabelisten"
},
"LIVE": "LIVE",
"Shared `x` ago": "Vor `x` geteilt",
"Unsubscribe": "Abbestellen",
"Unsubscribe": "Abo beenden",
"Subscribe": "Abonnieren",
"View channel on YouTube": "Kanal auf YouTube anzeigen",
"View playlist on YouTube": "Wiedergabeliste auf YouTube anzeigen",
@ -16,7 +25,7 @@
"Previous page": "Vorherige Seite",
"Clear watch history?": "Verlauf löschen?",
"New password": "Neues Passwort",
"New passwords must match": "Neue Passwörter müssen gleich sein",
"New passwords must match": "Neue Passwörter müssen übereinstimmen",
"Cannot change password for Google accounts": "Ich kann das Passwort deines Google Kontos nicht ändern",
"Authorize token?": "Token autorisieren?",
"Authorize token for `x`?": "Token für `x` autorisieren?",
@ -41,7 +50,7 @@
"Log in": "Anmelden",
"Log in/register": "Anmelden/registrieren",
"Log in with Google": "Mit Google anmelden",
"User ID": "Benutzer ID",
"User ID": "Benutzer-ID",
"Password": "Passwort",
"Time (h:mm:ss):": "Zeit (h:mm:ss):",
"Text CAPTCHA": "Text CAPTCHA",
@ -62,19 +71,23 @@
"Preferred video quality: ": "Bevorzugte Videoqualität: ",
"Player volume: ": "Wiedergabelautstärke: ",
"Default comments: ": "Standardkommentare: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Standarduntertitel: ",
"Fallback captions: ": "Ersatzuntertitel: ",
"Show related videos: ": "Ähnliche Videos anzeigen? ",
"Show annotations by default: ": "Standardmäßig Anmerkungen anzeigen? ",
"Automatically extend video description: ": "Videobeschreibung automatisch erweitern: ",
"Interactive 360 degree videos: ": "Interaktive 360 Grad Videos: ",
"Visual preferences": "Anzeigeeinstellungen",
"Player style: ": "Abspielgeräterstil: ",
"Dark mode: ": "Nachtmodus: ",
"Theme: ": "Modus: ",
"dark": "Nachtmodus",
"light": "klarer Modus",
"light": "heller Modus",
"Thin mode: ": "Schlanker Modus: ",
"Miscellaneous preferences": "Sonstige Einstellungen",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Automatische Instanzweiterleitung (über redirect.invidious.io): ",
"Subscription preferences": "Abonnementeinstellungen",
"Show annotations by default for subscribed channels: ": "Anmerkungen für abonnierte Kanäle standardmäßig anzeigen? ",
"Redirect homepage to feed: ": "Startseite zu Feed umleiten: ",
@ -95,7 +108,7 @@
"`x` is live": "`x` ist live",
"Data preferences": "Dateneinstellungen",
"Clear watch history": "Verlauf löschen",
"Import/export data": "Daten im-/exportieren",
"Import/export data": "Daten importieren/exportieren",
"Change password": "Passwort ändern",
"Manage subscriptions": "Abonnements verwalten",
"Manage tokens": "Tokens verwalten",
@ -104,6 +117,7 @@
"Administrator preferences": "Administrator-Einstellungen",
"Default homepage: ": "Standard-Startseite: ",
"Feed menu: ": "Feed-Menü: ",
"Show nickname on top: ": "Nutzernamen oben anzeigen: ",
"Top enabled: ": "Top aktiviert? ",
"CAPTCHA enabled: ": "CAPTCHA aktiviert? ",
"Login enabled: ": "Anmeldung aktiviert: ",
@ -113,16 +127,25 @@
"Subscription manager": "Abonnementverwaltung",
"Token manager": "Tokenverwalter",
"Token": "Token",
"`x` subscriptions": "`x` Abonnements",
"`x` tokens": "`x` Tokens",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Abonnements",
"": "`x` Abonnements"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Tokens",
"": "`x` Tokens"
},
"Import/export": "Importieren/Exportieren",
"unsubscribe": "abbestellen",
"revoke": "widerrufen",
"Subscriptions": "Abonnements",
"`x` unseen notifications": "`x` ungesehene Benachrichtigungen",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ungesehene Benachrichtigungen",
"": "`x` ungesehene Benachrichtigungen"
},
"search": "Suchen",
"Log out": "Abmelden",
"Released under the AGPLv3 by Omar Roth.": "Veröffentlicht unter AGPLv3 von Omar Roth.",
"Released under the AGPLv3 on Github.": "Auf Github unter der AGPLv3 Lizenz veröffentlicht.",
"Source available here.": "Quellcode verfügbar hier.",
"View JavaScript license information.": "Javascript Lizenzinformationen anzeigen.",
"View privacy policy.": "Datenschutzerklärung einsehen.",
@ -138,7 +161,11 @@
"Title": "Titel",
"Playlist privacy": "Vertrauliche Wiedergabeliste",
"Editing playlist `x`": "Wiedergabeliste bearbeiten `x`",
"Show more": "Mehr anzeigen",
"Show less": "Weniger anzeigen",
"Watch on YouTube": "Video auf YouTube ansehen",
"Switch Invidious Instance": "Invidious Instanz wechseln",
"Broken? Try another Invidious Instance": "Funktioniert nicht? Probiere eine andere Invidious Instanz aus",
"Hide annotations": "Anmerkungen ausblenden",
"Show annotations": "Anmerkungen anzeigen",
"Genre: ": "Genre: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Erlaubte Regionen: ",
"Blacklisted regions: ": "Unerlaubte Regionen: ",
"Shared `x`": "Geteilt `x`",
"`x` views": "`x` Aufrufe",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Aufrufe",
"": "`x` Aufrufe"
},
"Premieres in `x`": "Zuerst gesehen 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",
"View more comments on Reddit": "Mehr Kommentare auf Reddit anzeigen",
"View `x` comments": "`x` Kommentare anzeigen",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Kommentare anzeigen",
"": "`x` Kommentare anzeigen"
},
"View Reddit comments": "Reddit Kommentare anzeigen",
"Hide replies": "Antworten verstecken",
"Show replies": "Antworten anzeigen",
@ -180,15 +213,21 @@
"This channel does not exist.": "Dieser Kanal existiert nicht.",
"Could not get channel info.": "Kanalinformationen konnten nicht geladen werden.",
"Could not fetch comments": "Kommentare konnten nicht geladen werden",
"View `x` replies": "Zeige `x` Antworten",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Zeige `x` Antworten",
"": "Zeige `x` Antworten"
},
"`x` ago": "vor `x`",
"Load more": "Mehr laden",
"`x` points": "`x` Punkte",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Punkte",
"": "`x` Punkte"
},
"Could not create mix.": "Mix konnte nicht erstellt werden.",
"Empty playlist": "Playlist ist leer",
"Not a playlist.": "Ungültige Playlist.",
"Playlist does not exist.": "Playlist existiert nicht.",
"Could not pull trending pages.": "Trending Seiten konnten nicht geladen werden.",
"Empty playlist": "Wiedergabeliste ist leer",
"Not a playlist.": "Ungültige Wiedergabeliste.",
"Playlist does not exist.": "Wiedergabeliste existiert nicht.",
"Could not pull trending pages.": "Trendenz-Seiten konnten nicht geladen werden.",
"Hidden field \"challenge\" is a required field": "Verstecktes Feld „challenge“ ist eine erforderliche Eingabe",
"Hidden field \"token\" is a required field": "Verstecktes Feld „token“ ist eine erforderliche Eingabe",
"Erroneous challenge": "Ungültiger Test",
@ -301,15 +340,37 @@
"Yiddish": "Jiddisch",
"Yoruba": "Joruba",
"Zulu": "Zulu",
"`x` years": "`x` Jahre",
"`x` months": "`x` Monate",
"`x` weeks": "`x` Wochen",
"`x` days": "`x` Tage",
"`x` hours": "`x` Stunden",
"`x` minutes": "`x` Minuten",
"`x` seconds": "`x` Sekunden",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Jahre",
"": "`x` Jahre"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Monate",
"": "`x` Monate"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Wochen",
"": "`x` Wochen"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Tage",
"": "`x` Tage"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Stunden",
"": "`x` Stunden"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Minuten",
"": "`x` Minuten"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Sekunden",
"": "`x` Sekunden"
},
"Fallback comments: ": "Alternative Kommentare: ",
"Popular": "Populär",
"Search": "Suchen",
"Top": "Top",
"About": "Über",
"Rating: ": "Bewertung: ",
@ -332,5 +393,35 @@
"Videos": "Videos",
"Playlists": "Wiedergabelisten",
"Community": "Gemeinschaft",
"Current version: ": "Aktuelle Version: "
"relevance": "Relevanz",
"rating": "Bewertung",
"date": "Datum",
"views": "Aufrufe",
"content_type": "Inhaltstyp",
"duration": "Dauer",
"features": "Eigenschaften",
"sort": "sortieren",
"hour": "Letzte Stunde",
"today": "Heute",
"week": "Diese Woche",
"month": "Diesen Monat",
"year": "Dieses Jahr",
"video": "Video",
"channel": "Kanal",
"playlist": "Wiedergabeliste",
"movie": "Film",
"show": "Anzeigen",
"hd": "HD",
"subtitles": "Untertitel / CC",
"creative_commons": "Creative Commons",
"3d": "3D",
"live": "Live",
"4k": "4K",
"location": "Standort",
"hdr": "HDR",
"filter": "Filtern",
"Current version: ": "Aktuelle Version: ",
"next_steps_error_message": "Danach folgendes versuchen: ",
"next_steps_error_message_refresh": "Neuladen",
"next_steps_error_message_go_to_youtube": "Zu YouTube gehen"
}

View File

@ -1,9 +1,16 @@
{
"`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` συνδρομητές.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` subscribers.": "`x` συνδρομητές.",
"`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` βίντεο.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` videos.": "`x` βίντεο.",
"`x` playlists": "`x` λίστες αναπαραγωγής",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` συνδρομητές",
"": "`x` συνδρομητές"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` βίντεο",
"": "`x` βίντεο"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` λίστες αναπαραγωγής",
"": "`x` λίστες αναπαραγωγής"
},
"LIVE": "ΖΩΝΤΑΝΑ",
"Shared `x` ago": "Μοιράστηκε πριν από `x`",
"Unsubscribe": "Απεγγραφή",
@ -70,6 +77,8 @@
"Fallback captions: ": "Εναλλακτικοί υπότιτλοι: ",
"Show related videos: ": "Προβολή σχετικών βίντεο; ",
"Show annotations by default: ": "Αυτόματη προβολή σημειώσεων: ",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "Προτιμήσεις εμφάνισης",
"Player style: ": "Τεχνοτροπία της συσκευής αναπαραγωγης: ",
"Dark mode: ": "Σκοτεινή λειτουργία: ",
@ -77,6 +86,8 @@
"dark": "σκοτεινό",
"light": "φωτεινό",
"Thin mode: ": "Ελαφριά λειτουργία: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Προτιμήσεις συνδρομών",
"Show annotations by default for subscribed channels: ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ",
"Redirect homepage to feed: ": "Ανακατεύθυνση αρχικής στη ροή συνδρομών: ",
@ -106,6 +117,7 @@
"Administrator preferences": "Προτιμήσεις διαχειριστή",
"Default homepage: ": "Προεπιλεγμένη αρχική: ",
"Feed menu: ": "Μενού ροής συνδρομών: ",
"Show nickname on top: ": "",
"Top enabled: ": "Ενεργοποίηση κορυφαίων; ",
"CAPTCHA enabled: ": "Ενεργοποίηση CAPTCHA; ",
"Login enabled: ": "Ενεργοποίηση σύνδεσης; ",
@ -115,19 +127,25 @@
"Subscription manager": "Διαχειριστής συνδρομών",
"Token manager": "Διαχειριστής διασυνδέσεων",
"Token": "Διασύνδεση",
"`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` συνδρομή",
"`x` subscriptions.": "`x` συνδρομές.",
"`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` διασύνδεση",
"`x` tokens.": "`x` διασυνδέσεις.",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` συνδρομή",
"": "`x` συνδρομές"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` διασύνδεση",
"": "`x` διασυνδέσεις"
},
"Import/export": "Εισαγωγή/εξαγωγή",
"unsubscribe": "κατάργηση συνδρομής",
"revoke": "ανάκληση",
"Subscriptions": "Συνδρομές",
"`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` καινούρια ειδοποίηση",
"`x` unseen notifications.": "`x` καινούριες ειδοποιήσεις.",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` καινούρια ειδοποίηση",
"": "`x` καινούριες ειδοποιήσεις"
},
"search": "αναζήτηση",
"Log out": "Αποσύνδεση",
"Released under the AGPLv3 by Omar Roth.": "Κυκλοφορεί υπό την άδεια AGPLv3 από τον Omar Roth.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Προβολή πηγαίου κώδικα εδώ.",
"View JavaScript license information.": "Προβολή πληροφοριών άδειας JavaScript.",
"View privacy policy.": "Προβολή πολιτικής απορρήτου.",
@ -143,7 +161,11 @@
"Title": "Τίτλος",
"Playlist privacy": "Ιδιωτικότητα καταλόγων αναπαραγωγής",
"Editing playlist `x`": "Επεξεργασία `x` καταλόγου αναπαραγωγής",
"Show more": "",
"Show less": "",
"Watch on YouTube": "Προβολή στο YouTube",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "Απόκρυψη σημειώσεων",
"Show annotations": "Προβολή σημειώσεων",
"Genre: ": "Είδος: ",
@ -154,14 +176,19 @@
"Whitelisted regions: ": "Επιτρεπτές περιοχές: ",
"Blacklisted regions: ": "Μη-επιτρεπτές περιοχές: ",
"Shared `x`": "Μοιράστηκε το `x`",
"`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` προβολή",
"`x` views.": "`x` προβολές.",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` προβολή",
"": "`x` προβολές"
},
"Premieres in `x`": "Πρώτη προβολή σε `x`",
"Premieres `x`": "Επίσημη πρώτη παράσταση του `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.": "Γεια! Φαίνεται πως έχετε απενεργοποιήσει το JavaScript. Πατήστε εδώ για προβολή σχολίων, αλλά έχετε υπ'όψιν σας πως ίσως φορτώσουν πιο αργά.",
"View YouTube comments": "Προβολή σχολίων από το YouTube",
"View more comments on Reddit": "Προβολή περισσότερων σχολίων στο Reddit",
"View `x` comments": "Προβολή `x` σχολίων",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Προβολή `x` σχολίων",
"": "Προβολή `x` σχολίων"
},
"View Reddit comments": "Προβολή σχολίων από το Reddit",
"Hide replies": "Απόκρυψη απαντήσεων",
"Show replies": "Προβολή απαντήσεων",
@ -186,12 +213,16 @@
"This channel does not exist.": "Αυτό το κανάλι δεν υπάρχει.",
"Could not get channel info.": "Αδύναμια εύρεσης πληροφοριών καναλιού.",
"Could not fetch comments": "Αδυναμία λήψης σχολίων",
"View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "Προβολή `x` απάντησης",
"View `x` replies.": "Προβολή `x` απαντήσεων.",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Προβολή `x` απάντησης",
"": "Προβολή `x` απαντήσεων"
},
"`x` ago": "Πριν `x`",
"Load more": "Φόρτωση περισσότερων",
"`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "`x` βαθμός",
"`x` points.": "`x` βαθμοί.",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` βαθμός",
"": "`x` βαθμοί"
},
"Could not create mix.": "Αδυναμία δημιουργίας μίξης.",
"Empty playlist": "Κενή λίστα αναπαραγωγής",
"Not a playlist.": "Μη έγκυρη λίστα αναπαραγωγής.",
@ -309,22 +340,37 @@
"Yiddish": "Γίντις",
"Yoruba": "Γιορούμπα",
"Zulu": "Ζουλού",
"`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` χρόνο",
"`x` years.": "`x` χρόνια.",
"`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` μήνα",
"`x` months.": "`x` μήνες.",
"`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` εβδομάδα",
"`x` weeks.": "`x` εβδομάδες.",
"`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ημέρα",
"`x` days.": "`x` ημέρες.",
"`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ώρα",
"`x` hours.": "`x` ώρες.",
"`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` λεπτό",
"`x` minutes.": "`x` λεπτά.",
"`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` δευτερόλεπτο",
"`x` seconds.": "`x` δευτερόλεπτα.",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` χρόνο",
"": "`x` χρόνια"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` μήνα",
"": "`x` μήνες"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` εβδομάδα",
"": "`x` εβδομάδες"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ημέρα",
"": "`x` ημέρες"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ώρα",
"": "`x` ώρες"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` λεπτό",
"": "`x` λεπτά"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` δευτερόλεπτο",
"": "`x` δευτερόλεπτα"
},
"Fallback comments: ": "Εναλλακτικά σχόλια: ",
"Popular": "Δημοφιλή",
"Search": "",
"Top": "Κορυφαία",
"About": "Σχετικά",
"Rating: ": "Aξιολόγηση: ",
@ -347,5 +393,35 @@
"Videos": "Βίντεο",
"Playlists": "Λίστες Αναπαραγωγής",
"Community": "Κοινότητα",
"Current version: ": "Τρέχουσα έκδοση: "
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "Τρέχουσα έκδοση: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -71,12 +71,14 @@
"Preferred video quality: ": "Preferred video quality: ",
"Player volume: ": "Player volume: ",
"Default comments: ": "Default comments: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Default captions: ",
"Fallback captions: ": "Fallback captions: ",
"Show related videos: ": "Show related videos: ",
"Show annotations by default: ": "Show annotations by default: ",
"Automatically extend video description: ": "Automatically extend video description: ",
"Interactive 360 degree videos: ": "Interactive 360 degree videos: ",
"Visual preferences": "Visual preferences",
"Player style: ": "Player style: ",
"Dark mode: ": "Dark mode: ",
@ -84,6 +86,8 @@
"dark": "dark",
"light": "light",
"Thin mode: ": "Thin mode: ",
"Miscellaneous preferences": "Miscellaneous preferences",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Automaticatic instance redirection (fallback to redirect.invidious.io): ",
"Subscription preferences": "Subscription preferences",
"Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ",
"Redirect homepage to feed: ": "Redirect homepage to feed: ",
@ -113,6 +117,7 @@
"Administrator preferences": "Administrator preferences",
"Default homepage: ": "Default homepage: ",
"Feed menu: ": "Feed menu: ",
"Show nickname on top: ": "Show nickname on top: ",
"Top enabled: ": "Top enabled: ",
"CAPTCHA enabled: ": "CAPTCHA enabled: ",
"Login enabled: ": "Login enabled: ",
@ -140,7 +145,7 @@
},
"search": "search",
"Log out": "Log out",
"Released under the AGPLv3 by Omar Roth.": "Released under the AGPLv3 by Omar Roth.",
"Released under the AGPLv3 on Github.": "Released under the AGPLv3 on Github.",
"Source available here.": "Source available here.",
"View JavaScript license information.": "View JavaScript license information.",
"View privacy policy.": "View privacy policy.",
@ -156,7 +161,11 @@
"Title": "Title",
"Playlist privacy": "Playlist privacy",
"Editing playlist `x`": "Editing playlist `x`",
"Show more": "Show more",
"Show less": "Show less",
"Watch on YouTube": "Watch on YouTube",
"Switch Invidious Instance": "Switch Invidious Instance",
"Broken? Try another Invidious Instance": "Broken? Try another Invidious Instance",
"Hide annotations": "Hide annotations",
"Show annotations": "Show annotations",
"Genre: ": "Genre: ",
@ -361,6 +370,7 @@
},
"Fallback comments: ": "Fallback comments: ",
"Popular": "Popular",
"Search": "Search",
"Top": "Top",
"About": "About",
"Rating: ": "Rating: ",
@ -401,8 +411,8 @@
"playlist": "Playlist",
"movie": "Movie",
"show": "Show",
"short": "short",
"long": "long",
"short": "Short (< 4 minutes)",
"long": "Long (> 20 minutes)",
"hd": "HD",
"subtitles": "Subtitles/CC",
"creative_commons": "Creative Commons",
@ -412,5 +422,8 @@
"location": "Location",
"hdr": "HDR",
"filter": "Filter",
"Current version: ": "Current version: "
"Current version: ": "Current version: ",
"next_steps_error_message": "After which you should try to: ",
"next_steps_error_message_refresh": "Refresh",
"next_steps_error_message_go_to_youtube": "Go to YouTube"
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` abonantoj",
"`x` videos": "`x` filmetoj",
"`x` playlists": "`x` ludlistoj",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonantoj",
"": "`x` abonantoj"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` filmetoj",
"": "`x` filmetoj"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ludlistoj",
"": "`x` ludlistoj"
},
"LIVE": "NUNA",
"Shared `x` ago": "Konigita antaŭ `x`",
"Unsubscribe": "Malabonu",
@ -68,6 +77,8 @@
"Fallback captions: ": "Retrodefaŭltaj subtekstoj: ",
"Show related videos: ": "Ĉu montri rilatajn filmetojn? ",
"Show annotations by default: ": "Ĉu montri prinotojn defaŭlte? ",
"Automatically extend video description: ": "Aŭtomate etendi priskribon de filmeto: ",
"Interactive 360 degree videos: ": "Interagaj 360-gradaj filmetoj: ",
"Visual preferences": "Vidaj preferoj",
"Player style: ": "Ludila stilo: ",
"Dark mode: ": "Malhela reĝimo: ",
@ -75,6 +86,8 @@
"dark": "malhela",
"light": "hela",
"Thin mode: ": "Maldika reĝimo: ",
"Miscellaneous preferences": "Aliaj agordoj",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Aŭtomata alidirektado de instalaĵo (retropaŝo al redirect.invidious.io): ",
"Subscription preferences": "Abonaj agordoj",
"Show annotations by default for subscribed channels: ": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ",
"Redirect homepage to feed: ": "Alidirekti hejmpâgon al fluo: ",
@ -104,6 +117,7 @@
"Administrator preferences": "Agordoj de administranto",
"Default homepage: ": "Defaŭlta hejmpaĝo: ",
"Feed menu: ": "Flua menuo: ",
"Show nickname on top: ": "Montri kromnomon supre: ",
"Top enabled: ": "Ĉu pli bonaj ŝaltitaj? ",
"CAPTCHA enabled: ": "Ĉu CAPTCHA ŝaltita? ",
"Login enabled: ": "Ĉu ensaluto aktivita? ",
@ -113,16 +127,25 @@
"Subscription manager": "Administrilo de abonoj",
"Token manager": "Ĵetona administrilo",
"Token": "Ĵetono",
"`x` subscriptions": "`x` abonoj",
"`x` tokens": "`x` ĵetonoj",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonoj",
"": "`x` abonoj"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ĵetonoj",
"": "`x` ĵetonoj"
},
"Import/export": "Importi/Eksporti",
"unsubscribe": "malabonu",
"revoke": "senvalidigi",
"Subscriptions": "Abonoj",
"`x` unseen notifications": "`x` neviditaj sciigoj",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` neviditaj sciigoj",
"": "`x` neviditaj sciigoj"
},
"search": "serĉi",
"Log out": "Elsaluti",
"Released under the AGPLv3 by Omar Roth.": "Eldonita sub la AGPLv3 de Omar Roth.",
"Released under the AGPLv3 on Github.": "Eldonita sub la AGPLv3 en Github.",
"Source available here.": "Fonto havebla ĉi tie.",
"View JavaScript license information.": "Vidi Ĝavoskriptan licencan informon.",
"View privacy policy.": "Vidi regularon pri privateco.",
@ -138,7 +161,11 @@
"Title": "Titolo",
"Playlist privacy": "Privateco de ludlisto",
"Editing playlist `x`": "Redaktante ludlisto `x`",
"Show more": "Montri pli",
"Show less": "Montri malpli",
"Watch on YouTube": "Vidi filmeton en JuTubo",
"Switch Invidious Instance": "Ŝanĝi instalaĵon de Indivious",
"Broken? Try another Invidious Instance": "Ĉu misfunkcio? Provu alian instalaĵon de Indivious",
"Hide annotations": "Kaŝi prinotojn",
"Show annotations": "Montri prinotojn",
"Genre: ": "Ĝenro: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Regionoj listigitaj en blanka listo: ",
"Blacklisted regions: ": "Regionoj listigitaj en nigra listo: ",
"Shared `x`": "Konigita `x`",
"`x` views": "`x` spektaĵoj",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` spektaĵoj",
"": "`x` spektaĵoj"
},
"Premieres in `x`": "Premieras en `x`",
"Premieres `x`": "Premieras `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.": "Saluton! Ŝajnas, ke vi havas Ĝavoskripton malebligitan. Klaku ĉi tie por vidi komentojn, memoru, ke la ŝargado povus daŭri iom pli.",
"View YouTube comments": "Vidi komentojn de JuTubo",
"View more comments on Reddit": "Vidi pli komentoj en Reddit",
"View `x` comments": "Vidi `x` komentojn",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Vidi `x` komentojn",
"": "Vidi `x` komentojn"
},
"View Reddit comments": "Vidi komentojn de Reddit",
"Hide replies": "Kaŝi respondojn",
"Show replies": "Montri respondojn",
@ -180,10 +213,16 @@
"This channel does not exist.": "Ĉi tiu kanalo ne ekzistas.",
"Could not get channel info.": "Ne povis havigi kanalan informon.",
"Could not fetch comments": "Ne povis venigi komentojn",
"View `x` replies": "Vidi `x` respondojn",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Vidi `x` respondojn",
"": "Vidi `x` respondojn"
},
"`x` ago": "antaŭ `x`",
"Load more": "Ŝarĝi pli",
"`x` points": "`x` poentoj",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` poentoj",
"": "`x` poentoj"
},
"Could not create mix.": "Ne povis krei mikson.",
"Empty playlist": "Ludlisto estas malplena",
"Not a playlist.": "Nevalida ludlisto.",
@ -301,15 +340,37 @@
"Yiddish": "Jida",
"Yoruba": "Joruba",
"Zulu": "Zulua",
"`x` years": "`x` jaroj",
"`x` months": "`x` monatoj",
"`x` weeks": "`x` semajnoj",
"`x` days": "`x` tagoj",
"`x` hours": "`x` horoj",
"`x` minutes": "`x` minutoj",
"`x` seconds": "`x` sekundoj",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` jaroj",
"": "`x` jaroj"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` monatoj",
"": "`x` monatoj"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` semajnoj",
"": "`x` semajnoj"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tagoj",
"": "`x` tagoj"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` horoj",
"": "`x` horoj"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minutoj",
"": "`x` minutoj"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` sekundoj",
"": "`x` sekundoj"
},
"Fallback comments: ": "Retrodefaŭltaj komentoj: ",
"Popular": "Popularaj",
"Search": "Serĉi",
"Top": "Supraj",
"About": "Pri",
"Rating: ": "Takso: ",
@ -332,5 +393,35 @@
"Videos": "Filmetoj",
"Playlists": "Ludlistoj",
"Community": "Komunumo",
"Current version: ": "Nuna versio: "
"relevance": "rilateco",
"rating": "takso",
"date": "dato",
"views": "vidoj",
"content_type": "enhavtipo",
"duration": "daŭro",
"features": "trajtoj",
"sort": "ordigi",
"hour": "horo",
"today": "hodiaŭ",
"week": "semajno",
"month": "monato",
"year": "jaro",
"video": "filmeto",
"channel": "kanalo",
"playlist": "ludlisto",
"movie": "filmo",
"show": "spektaĵo",
"hd": "altdistingiva",
"subtitles": "subtekstoj",
"creative_commons": "Krea Komunaĵo",
"3d": "3D",
"live": "nuna",
"4k": "4k",
"location": "loko",
"hdr": "granddinamikgama",
"filter": "filtri",
"Current version: ": "Nuna versio: ",
"next_steps_error_message": "Poste, vi provu: ",
"next_steps_error_message_refresh": "Reŝargi",
"next_steps_error_message_go_to_youtube": "Iri al JuTubo"
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` suscriptores",
"`x` videos": "`x` vídeos",
"`x` playlists": "`x` listas de reproducción",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` suscriptores",
"": "`x` suscriptores"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` vídeos",
"": "`x` vídeos"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` listas de reproducción",
"": "`x` listas de reproducción"
},
"LIVE": "DIRECTO",
"Shared `x` ago": "Compartido hace `x`",
"Unsubscribe": "Desuscribirse",
@ -68,6 +77,8 @@
"Fallback captions: ": "Subtítulos alternativos: ",
"Show related videos: ": "¿Mostrar vídeos relacionados? ",
"Show annotations by default: ": "¿Mostrar anotaciones por defecto? ",
"Automatically extend video description: ": "Extender automáticamente la descripción del vídeo: ",
"Interactive 360 degree videos: ": "Vídeos interactivos de 360 grados: ",
"Visual preferences": "Preferencias visuales",
"Player style: ": "Estilo de reproductor: ",
"Dark mode: ": "Modo oscuro: ",
@ -75,6 +86,8 @@
"dark": "oscuro",
"light": "claro",
"Thin mode: ": "Modo compacto: ",
"Miscellaneous preferences": "Preferencias misceláneas",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Redirección automática de instancia (segunda opción a redirect.invidious.io): ",
"Subscription preferences": "Preferencias de la suscripción",
"Show annotations by default for subscribed channels: ": "¿Mostrar anotaciones por defecto para los canales suscritos? ",
"Redirect homepage to feed: ": "Redirigir la página de inicio a la fuente: ",
@ -104,6 +117,7 @@
"Administrator preferences": "Preferencias de administrador",
"Default homepage: ": "Página de inicio por defecto: ",
"Feed menu: ": "Menú de fuentes: ",
"Show nickname on top: ": "Mostrar nombre de usuario arriba: ",
"Top enabled: ": "¿Habilitar los destacados? ",
"CAPTCHA enabled: ": "¿Habilitar los CAPTCHA? ",
"Login enabled: ": "¿Habilitar el inicio de sesión? ",
@ -113,16 +127,25 @@
"Subscription manager": "Gestor de suscripciones",
"Token manager": "Gestor de tokens",
"Token": "Token",
"`x` subscriptions": "`x` suscripciones",
"`x` tokens": "`x` tokens",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` suscripciones",
"": "`x` suscripciones"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens",
"": "`x` tokens"
},
"Import/export": "Importar/Exportar",
"unsubscribe": "Desuscribirse",
"revoke": "revocar",
"Subscriptions": "Suscripciones",
"`x` unseen notifications": "`x` notificaciones sin ver",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificaciones sin ver",
"": "`x` notificaciones sin ver"
},
"search": "buscar",
"Log out": "Cerrar la sesión",
"Released under the AGPLv3 by Omar Roth.": "Publicado bajo licencia AGPLv3 por Omar Roth.",
"Released under the AGPLv3 on Github.": "Publicado bajo la AGPLv3 en Github.",
"Source available here.": "Código fuente disponible aquí.",
"View JavaScript license information.": "Ver información de licencia de JavaScript.",
"View privacy policy.": "Ver la política de privacidad.",
@ -138,7 +161,11 @@
"Title": "Título",
"Playlist privacy": "Privacidad de la lista de reproducción",
"Editing playlist `x`": "Editando la lista de reproducción 'x'",
"Watch on YouTube": "Ver el vídeo en Youtube",
"Show more": "Mostrar más",
"Show less": "Mostrar menos",
"Watch on YouTube": "Ver el vídeo en YouTube",
"Switch Invidious Instance": "Cambiar Instancia de Invidious",
"Broken? Try another Invidious Instance": "¿Algún error? Prueba otra instancia de Invidious",
"Hide annotations": "Ocultar anotaciones",
"Show annotations": "Mostrar anotaciones",
"Genre: ": "Género: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Regiones permitidas: ",
"Blacklisted regions: ": "Regiones bloqueadas: ",
"Shared `x`": "Compartido `x`",
"`x` views": "`x` visualizaciones",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizaciones",
"": "`x` visualizaciones"
},
"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 tiene JavaScript desactivado. Haga clic aquí para ver los comentarios, pero tenga en cuenta que pueden tardar un poco más en cargarse.",
"View YouTube comments": "Ver los comentarios de YouTube",
"View more comments on Reddit": "Ver más comentarios en Reddit",
"View `x` comments": "Ver `x` comentarios",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentarios",
"": "Ver `x` comentarios"
},
"View Reddit comments": "Ver los comentarios de Reddit",
"Hide replies": "Ocultar las respuestas",
"Show replies": "Mostrar las respuestas",
@ -180,10 +213,16 @@
"This channel does not exist.": "El canal no existe.",
"Could not get channel info.": "No se ha podido obtener información del canal.",
"Could not fetch comments": "No se han podido recuperar los comentarios",
"View `x` replies": "Ver `x` respuestas",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` respuestas",
"": "Ver `x` respuestas"
},
"`x` ago": "hace `x`",
"Load more": "Cargar más",
"`x` points": "`x` puntos",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` puntos",
"": "`x` puntos"
},
"Could not create mix.": "No se ha podido crear la mezcla.",
"Empty playlist": "La lista de reproducción está vacía",
"Not a playlist.": "Lista de reproducción no válida.",
@ -301,15 +340,37 @@
"Yiddish": "Yidis",
"Yoruba": "Yoruba",
"Zulu": "Zulú",
"`x` years": "`x` años",
"`x` months": "`x` meses",
"`x` weeks": "`x` semanas",
"`x` days": "`x` días",
"`x` hours": "`x` horas",
"`x` minutes": "`x` minutos",
"`x` seconds": "`x` segundos",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` año",
"": "`x` años"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` meses",
"": "`x` meses"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` semanas",
"": "`x` semanas"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` días",
"": "`x` días"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` horas",
"": "`x` horas"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minutos",
"": "`x` minutos"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` segundos",
"": "`x` segundos"
},
"Fallback comments: ": "Comentarios alternativos: ",
"Popular": "Populares",
"Search": "Buscar",
"Top": "Destacados",
"About": "Acerca de",
"Rating: ": "Valoración: ",
@ -332,5 +393,35 @@
"Videos": "Vídeos",
"Playlists": "Listas de reproducción",
"Community": "Comunidad",
"Current version: ": "Versión actual: "
"relevance": "relevancia",
"rating": "valoración",
"date": "fecha",
"views": "visualizaciones",
"content_type": "content_type",
"duration": "duración",
"features": "funcionalidades",
"sort": "ordenar",
"hour": "hora",
"today": "hoy",
"week": "semana",
"month": "mes",
"year": "año",
"video": "vídeo",
"channel": "canal",
"playlist": "lista de reproducción",
"movie": "película",
"show": "programa",
"hd": "hd",
"subtitles": "subtítulos",
"creative_commons": "creative_commons",
"3d": "3d",
"live": "directo",
"4k": "4k",
"location": "ubicación",
"hdr": "hdr",
"filter": "filtro",
"Current version: ": "Versión actual: ",
"next_steps_error_message": "Después de lo cual deberías intentar: ",
"next_steps_error_message_refresh": "Recargar",
"next_steps_error_message_go_to_youtube": "Ir a YouTube"
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` harpidedun",
"`x` videos": "`x` bideo",
"`x` playlists": "`x` erreprodukzio-zerrenda",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` harpidedunak",
"": "`x` harpidedun"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` bideoak",
"": "`x` bideo"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` erreprodukzio-zerrenda"
},
"LIVE": "ZUZENEAN",
"Shared `x` ago": "Duela `x` partekatua",
"Unsubscribe": "Harpidetza kendu",
@ -62,12 +71,14 @@
"Preferred video quality: ": "Hobetsitako bideoaren kalitatea: ",
"Player volume: ": "Erreproduzigailuaren bolumena: ",
"Default comments: ": "Lehenetsitako iruzkinak: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Lehenetsitako azpitituluak: ",
"Fallback captions: ": "",
"Show related videos: ": "Erakutsi erlazionatutako bideoak: ",
"Show annotations by default: ": "Erakutsi oharrak modu lehenetsian: ",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "Hobespen bisualak",
"Player style: ": "Erreproduzigailu mota: ",
"Dark mode: ": "Gai iluna: ",
@ -75,6 +86,8 @@
"dark": "iluna",
"light": "argia",
"Thin mode: ": "",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Harpidetzen hobespenak",
"Show annotations by default for subscribed channels: ": "",
"Redirect homepage to feed: ": "",
@ -104,6 +117,7 @@
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Show nickname on top: ": "",
"Top enabled: ": "",
"CAPTCHA enabled: ": "",
"Login enabled: ": "",
@ -113,16 +127,25 @@
"Subscription manager": "",
"Token manager": "",
"Token": "",
"`x` subscriptions": "",
"`x` tokens": "",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Import/export": "",
"unsubscribe": "",
"revoke": "",
"Subscriptions": "",
"`x` unseen notifications": "",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"search": "",
"Log out": "",
"Released under the AGPLv3 by Omar Roth.": "",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "",
"View JavaScript license information.": "",
"View privacy policy.": "",
@ -138,7 +161,11 @@
"Title": "",
"Playlist privacy": "",
"Editing playlist `x`": "",
"Show more": "",
"Show less": "",
"Watch on YouTube": "",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "",
"Show annotations": "",
"Genre: ": "",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "",
"Blacklisted regions: ": "",
"Shared `x`": "",
"`x` views": "",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Premieres in `x`": "",
"Premieres `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.": "",
"View YouTube comments": "",
"View more comments on Reddit": "",
"View `x` comments": "",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"View Reddit comments": "",
"Hide replies": "",
"Show replies": "",
@ -180,10 +213,16 @@
"This channel does not exist.": "",
"Could not get channel info.": "",
"Could not fetch comments": "",
"View `x` replies": "",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` ago": "",
"Load more": "",
"`x` points": "",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Could not create mix.": "",
"Empty playlist": "",
"Not a playlist.": "",
@ -301,15 +340,37 @@
"Yiddish": "",
"Yoruba": "",
"Zulu": "",
"`x` years": "",
"`x` months": "",
"`x` weeks": "",
"`x` days": "",
"`x` hours": "",
"`x` minutes": "",
"`x` seconds": "",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Fallback comments: ": "",
"Popular": "",
"Search": "",
"Top": "",
"About": "",
"Rating: ": "",
@ -332,5 +393,35 @@
"Videos": "",
"Playlists": "",
"Community": "",
"Current version: ": ""
}
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,10 +1,16 @@
{
"`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` مشترکان.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` subscribers.": "`x` مشترکان.",
"`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ویدیو ها.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` videos.": "`x` ویدیو ها.",
"`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "`x` لیست های پخش.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` playlists.": "`x` لیست های پخش.",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` مشترکان",
"": "`x` مشترکان"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ویدیو ها",
"": "`x` ویدیو ها"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` لیست های پخش",
"": "`x` لیست های پخش"
},
"LIVE": "زنده",
"Shared `x` ago": "به اشتراک گذاشته شده `x` پیش",
"Unsubscribe": "لغو اشتراک",
@ -71,6 +77,8 @@
"Fallback captions: ": "عقب گرد زیرنویس ها: ",
"Show related videos: ": "نمایش ویدیو های مرتبط: ",
"Show annotations by default: ": "نمایش حاشیه نویسی ها به طور پیشفرض: ",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "ترجیحات بصری",
"Player style: ": "حالت پخش کننده: ",
"Dark mode: ": "حالت تاریک: ",
@ -78,6 +86,8 @@
"dark": "تاریک",
"light": "روشن",
"Thin mode: ": "حالت نازک: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "ترجیحات اشتراک",
"Show annotations by default for subscribed channels: ": "نمایش حاشیه نویسی ها به طور پیشفرض برای کانال های مشترک شده: ",
"Redirect homepage to feed: ": "تغییر مسیر صفحه خانه به خوراک: ",
@ -107,6 +117,7 @@
"Administrator preferences": "ترجیحات مدیریت",
"Default homepage: ": "صفحه خانه پیشفرض ",
"Feed menu: ": "منو خوراک: ",
"Show nickname on top: ": "",
"Top enabled: ": "بالا فعال شده: ",
"CAPTCHA enabled: ": "CAPTCHA فعال شده: ",
"Login enabled: ": "ورود فعال شده: ",
@ -116,19 +127,25 @@
"Subscription manager": "مدیریت اشتراک",
"Token manager": "مدیر توکن",
"Token": "توکن",
"`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` اشتراک ها.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` subscriptions.": "`x` اشتراک ها.",
"`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` توکن ها.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` tokens.": "`x` توکن ها.",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` اشتراک ها",
"": "`x` اشتراک ها"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` توکن ها",
"": "`x` توکن ها"
},
"Import/export": "وارد کردن/خارج کردن",
"unsubscribe": "لغو اشتراک",
"revoke": "ابطال",
"Subscriptions": "اشتراک ها",
"`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` اعلان نادیده.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` unseen notifications.": "`x` اعلان نادیده.",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` اعلان نادیده",
"": "`x` اعلان نادیده"
},
"search": "جستجو",
"Log out": "خروج",
"Released under the AGPLv3 by Omar Roth.": "منتشر شده تحت مجوز AGPLv3 توسط Omar Roth.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "منبع اینجا دردسترس است.",
"View JavaScript license information.": "نمایش اطلاعات مجوز جاوا اسکریپت.",
"View privacy policy.": "نمایش سیاست حفظ حریم خصوصی.",
@ -144,7 +161,11 @@
"Title": "عنوان",
"Playlist privacy": "حریم خصوصی لیست پخش",
"Editing playlist `x`": "تغییر لیست پخش `x`",
"Show more": "",
"Show less": "",
"Watch on YouTube": "تماشا در یوتیوب",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "مخفی کردن حاشیه نویسی ها",
"Show annotations": "نمایش حاشیه نویسی ها",
"Genre: ": "ژانر: ",
@ -155,15 +176,19 @@
"Whitelisted regions: ": "مناطق لیست سفید: ",
"Blacklisted regions: ": "مناطق لیست سیاه: ",
"Shared `x`": "به اشتراک گذاشته شده `x`",
"`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` بازدید.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` views.": "`x` بازدید.",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` بازدید",
"": "`x` بازدید"
},
"Premieres in `x`": "برای اولین بار در `x`",
"Premieres `x`": "برای اولین بار `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.": "سلام! مثل اینکه تو جاوا اسکریپت رو خاموش کرده ای. اینجا کلیک کن تا نظرات را ببینی، این رو یادت باشه که ممکنه بارگذاری اونها کمی طول بکشه.",
"View YouTube comments": "نمایش نظرات یوتیوب",
"View more comments on Reddit": "نمایش نظرات بیشتر در ردیت",
"View `x` comments.([^.,0-9]|^)1([^.,0-9]|$)": "نمایش `x` نظرات.([^.,0-9]|^)1([^.,0-9]|$)",
"View `x` comments.": "نمایش `x` نظرات.",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "نمایش `x` نظرات",
"": "نمایش `x` نظرات"
},
"View Reddit comments": "نمایش نظرات ردیت",
"Hide replies": "مخفی کردن پاسخ ها",
"Show replies": "نمایش پاسخ ها",
@ -188,12 +213,16 @@
"This channel does not exist.": "این کانال وجود ندارد.",
"Could not get channel info.": "نمیتوان اطلاعات کانال را دریافت کرد.",
"Could not fetch comments": "نمیتوان نظرات را دریافت کرد",
"View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "نمایش `x` پاسخ ها.([^.,0-9]|^)1([^.,0-9]|$)",
"View `x` replies.": "نمایش `x` پاسخ ها.",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "نمایش `x` پاسخ ها",
"": "نمایش `x` پاسخ ها"
},
"`x` ago": "`x` پیش",
"Load more": "بارگذاری بیشتر",
"`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "`x` نقطه ها.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` points.": "`x` نقطه ها.",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` نقطه ها",
"": "`x` نقطه ها"
},
"Could not create mix.": "نمیتوان میکس ساخت.",
"Empty playlist": "لیست پخش خالی",
"Not a playlist.": "یک لیست پخش نیست.",
@ -311,22 +340,37 @@
"Yiddish": "ییدیش",
"Yoruba": "یوروبایی",
"Zulu": "زولو",
"`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` سال.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` years.": "`x` سال.",
"`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ماه.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` months.": "`x` ماه.",
"`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` هفته.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` weeks.": "`x` هفته.",
"`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` روز.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` days.": "`x` روز.",
"`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ساعت.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` hours.": "`x` ساعت.",
"`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` دقیقه.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` minutes.": "`x` دقیقه.",
"`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ثانیه.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` seconds.": "`x` ثانیه.",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` سال",
"": "`x` سال"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ماه",
"": "`x` ماه"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` هفته",
"": "`x` هفته"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` روز",
"": "`x` روز"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ساعت",
"": "`x` ساعت"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` دقیقه",
"": "`x` دقیقه"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ثانیه",
"": "`x` ثانیه"
},
"Fallback comments: ": "نظرات عقب گرد: ",
"Popular": "محبوب",
"Search": "",
"Top": "بالا",
"About": "درباره",
"Rating: ": "رتبه دهی: ",
@ -349,5 +393,35 @@
"Videos": "ویدیو ها",
"Playlists": "لیست های پخش",
"Community": "اجتماع",
"Current version: ": "نسخه فعلی: "
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "نسخه فعلی: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,10 +1,16 @@
{
"`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` tilaaja",
"`x` subscribers.": "`x` tilaajaa.",
"`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` video",
"`x` videos.": "`x` videota.",
"`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "`x` soittolista.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` playlists.": "`x` soittolistaa.",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tilaaja",
"": "`x` tilaajaa"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` video",
"": "`x` videota"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` soittolista",
"": "`x` soittolistaa"
},
"LIVE": "SUORA",
"Shared `x` ago": "Jaettu `x` sitten",
"Unsubscribe": "Peruuta tilaus",
@ -71,6 +77,8 @@
"Fallback captions: ": "Toissijaiset tekstitykset: ",
"Show related videos: ": "Näytä aiheeseen liittyviä videoita: ",
"Show annotations by default: ": "Näytä huomautukset oletuksena: ",
"Automatically extend video description: ": "Laajenna automaattisesti videon kuvausta: ",
"Interactive 360 degree videos: ": "Interaktiiviset 360-asteiset videot: ",
"Visual preferences": "Visuaaliset asetukset",
"Player style: ": "Soittimen tyyli: ",
"Dark mode: ": "Tumma tila: ",
@ -78,6 +86,8 @@
"dark": "tumma",
"light": "vaalea",
"Thin mode: ": "Kapea tila ",
"Miscellaneous preferences": "Sekalaiset asetukset",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Automaattinen palveluntarjoajan uudelleenohjaus (perääntyminen sivulle redirect.invidious.io) ",
"Subscription preferences": "Tilausten asetukset",
"Show annotations by default for subscribed channels: ": "Näytä oletuksena tilattujen kanavien huomautukset: ",
"Redirect homepage to feed: ": "Uudelleenohjaa kotisivu syötteeseen: ",
@ -107,6 +117,7 @@
"Administrator preferences": "Järjestelmänvalvojan asetukset",
"Default homepage: ": "Oletuskotisivu: ",
"Feed menu: ": "Syötevalikko: ",
"Show nickname on top: ": "Näytä nimimerkki ylimpänä: ",
"Top enabled: ": "Yläosa käytössä: ",
"CAPTCHA enabled: ": "CAPTCHA käytössä: ",
"Login enabled: ": "Kirjautuminen käytössä: ",
@ -116,19 +127,25 @@
"Subscription manager": "Tilausten hallinnoija",
"Token manager": "Tunnusten hallinnoija",
"Token": "Tunnus",
"`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` tilausta.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` subscriptions.": "`x` tilausta.",
"`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` tunnistetta.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` tokens.": "`x` tunnistetta.",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tilausta",
"": "`x` tilausta"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tunnistetta",
"": "`x` tunnistetta"
},
"Import/export": "Tuo/vie",
"unsubscribe": "peru tilaus",
"revoke": "kumoa",
"Subscriptions": "Tilaukset",
"`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` näkemätöntä ilmoitusta.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` unseen notifications.": "`x` näkemätöntä ilmoitusta.",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` näkemätöntä ilmoitusta",
"": "`x` näkemätöntä ilmoitusta"
},
"search": "haku",
"Log out": "Kirjaudu ulos",
"Released under the AGPLv3 by Omar Roth.": "Julkaissut AGPLv3-lisenssillä: Omar Roth.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Lähdekoodi on saatavilla täällä.",
"View JavaScript license information.": "JavaScript-koodin lisenssit.",
"View privacy policy.": "Katso tietosuojaseloste.",
@ -144,7 +161,11 @@
"Title": "Nimi",
"Playlist privacy": "Soittolistan yksityisyys",
"Editing playlist `x`": "Muokataan soittolistaa `x`",
"Show more": "Näytä enemmän",
"Show less": "Näytä vähemmän",
"Watch on YouTube": "Katso YouTubessa",
"Switch Invidious Instance": "Vaihda Invidious-palveluntarjoajaa",
"Broken? Try another Invidious Instance": "Rikki? Kokeile toista Invidious-palveluntarjoajaa",
"Hide annotations": "Piilota merkkaukset",
"Show annotations": "Näytä merkkaukset",
"Genre: ": "Genre: ",
@ -152,18 +173,22 @@
"Family friendly? ": "Kaiken ikäisille sopiva? ",
"Wilson score: ": "Wilson-pistemäärä: ",
"Engagement: ": "Huomio: ",
"Whitelisted regions: ": "valkolistatut alueet: ",
"Blacklisted regions: ": "mustalla listalla olevat alueet: ",
"Whitelisted regions: ": "Sallitut alueet: ",
"Blacklisted regions: ": "Estetyt alueet: ",
"Shared `x`": "Jaettu `x`",
"`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` katselukertaa.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` views.": "`x` katselukertaa.",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` katselukerta",
"": "`x` katselukertaa"
},
"Premieres in `x`": "Ensiesitykseen aikaa `x`",
"Premieres `x`": "Ensiesitykseen `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.": "Hei! Vaikuttaa siltä, että sinulla on JavaScript pois käytöstä. Klikkaa tästä nähdäksesi kommentit, huomioi että lataamisessa voi kestää melko kauan.",
"View YouTube comments": "Näytä YouTube-kommentit",
"View more comments on Reddit": "Katso lisää kommentteja Redditissä",
"View `x` comments.([^.,0-9]|^)1([^.,0-9]|$)": "Näytä `x` komenttia.([^.,0-9]|^)1([^.,0-9]|$)",
"View `x` comments.": "Näytä `x` kommenttia.",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Näytä `x` komenttia",
"": "Näytä `x` kommenttia"
},
"View Reddit comments": "Näytä Reddit-kommentit",
"Hide replies": "Piilota vastaukset",
"Show replies": "Näytä vastaukset",
@ -188,18 +213,22 @@
"This channel does not exist.": "Tätä kanavaa ei ole olemassa.",
"Could not get channel info.": "Kanavatietoa ei saatu ladattua.",
"Could not fetch comments": "Kommenttien nouto epäonnistui",
"View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "Näytä `x` vastausta.([^.,0-9]|^)1([^.,0-9]|$)",
"View `x` replies.": "Näytä `x` vastausta.",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Näytä `x` vastausta",
"": "Näytä `x` vastausta"
},
"`x` ago": "`x` sitten",
"Load more": "Lataa lisää",
"`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "`x` pistettä.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` points.": "`x` pistettä.",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pistettä",
"": "`x` pistettä"
},
"Could not create mix.": "Sekoituksen luominen epäonnistui.",
"Empty playlist": "Tyhjennä soittolista",
"Not a playlist.": "Ei ole soittolista.",
"Playlist does not exist.": "Soittolistaa ei ole olemassa.",
"Could not pull trending pages.": "Nousussa olevien sivujen lataus epäonnitui.",
"Hidden field \"challenge\" is a required field": "Piilotettu kenttä \"challenge\" on vaaditaan",
"Could not pull trending pages.": "Nousussa olevien sivujen lataus epäonnistui.",
"Hidden field \"challenge\" is a required field": "Piilotettu kenttä \"challenge\" vaaditaan",
"Hidden field \"token\" is a required field": "Piilotettu kenttä \"tunniste\" vaaditaan",
"Erroneous challenge": "Virheellinen haaste",
"Erroneous token": "Virheellinen tunniste",
@ -311,22 +340,37 @@
"Yiddish": "jiddiš",
"Yoruba": "joruba",
"Zulu": "zulu",
"`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` vuotta.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` years.": "`x` vuotta.",
"`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` kuukautta.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` months.": "`x` kuukautta.",
"`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` viikkoa.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` weeks.": "`x` viikkoa.",
"`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` päivää.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` days.": "`x` päivää.",
"`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` tuntia.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` hours.": "`x` tuntia.",
"`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuuttia.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` minutes.": "`x` minuuttia.",
"`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` sekuntia.([^.,0-9]|^)1([^.,0-9]|$)",
"`x` seconds.": "`x` sekuntia.",
"Fallback comments: ": "varakommentit: ",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` vuotta",
"": "`x` vuotta"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` kuukautta",
"": "`x` kuukautta"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` viikkoa",
"": "`x` viikkoa"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` päivää",
"": "`x` päivää"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tuntia",
"": "`x` tuntia"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuuttia",
"": "`x` minuuttia"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` sekuntia",
"": "`x` sekuntia"
},
"Fallback comments: ": "Varakommentit: ",
"Popular": "Suosittu",
"Search": "Etsi",
"Top": "Ylin",
"About": "Tietoa",
"Rating: ": "Arvosana: ",
@ -349,5 +393,35 @@
"Videos": "Videot",
"Playlists": "Soittolistat",
"Community": "Yhteisö",
"Current version: ": "Tämänhetkinen versio: "
"relevance": "Osuvuus",
"rating": "Arvostelu",
"date": "Latauspäivämäärä",
"views": "Katselukerrat",
"content_type": "Tyyppi",
"duration": "Kesto",
"features": "Ominaisuudet",
"sort": "Luokittele",
"hour": "Viimeisin tunti",
"today": "Tänään",
"week": "Tämä viikko",
"month": "Tämä kuukausi",
"year": "Tämä vuosi",
"video": "Video",
"channel": "Kanava",
"playlist": "Soittolista",
"movie": "Elokuva",
"show": "Ohjelma",
"hd": "HD",
"subtitles": "Tekstitys/CC",
"creative_commons": "Creative Commons",
"3d": "3D",
"live": "Suora lähetys",
"4k": "4K",
"location": "Sijainti",
"hdr": "HDR",
"filter": "Suodatin",
"Current version: ": "Tämänhetkinen versio: ",
"next_steps_error_message": "Sinun tulisi kokeilla seuraavia: ",
"next_steps_error_message_refresh": "Päivitä",
"next_steps_error_message_go_to_youtube": "Siirry YouTubeen"
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` abonnés",
"`x` videos": "`x` vidéos",
"`x` playlists": "`x` listes de lecture",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonné",
"": "`x` abonnés"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` vidéo",
"": "`x` vidéos"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` liste de lecture",
"": "`x` listes de lecture"
},
"LIVE": "EN DIRECT",
"Shared `x` ago": "Ajoutée il y a `x`",
"Unsubscribe": "Se désabonner",
@ -18,7 +27,7 @@
"New password": "Nouveau mot de passe",
"New passwords must match": "Les nouveaux mots de passe doivent correspondre",
"Cannot change password for Google accounts": "Le mot de passe d'un compte Google ne peut pas être changé depuis Invidious",
"Authorize token?": "Autoriser le token ?",
"Authorize token?": "Autoriser le token ?",
"Authorize token for `x`?": "Autoriser le token pour `x` ?",
"Yes": "Oui",
"No": "Non",
@ -39,13 +48,13 @@
"JavaScript license information": "Informations sur les licences JavaScript",
"source": "source",
"Log in": "Se connecter",
"Log in/register": "Se connecter/Créer un compte",
"Log in/register": "Se connecter/S'inscrire",
"Log in with Google": "Se connecter avec Google",
"User ID": "Identifiant utilisateur",
"Password": "Mot de passe",
"Time (h:mm:ss):": "Heure (h:mm:ss) :",
"Text CAPTCHA": "CAPTCHA Texte",
"Image CAPTCHA": "CAPTCHA Image",
"Text CAPTCHA": "CAPTCHA textuel",
"Image CAPTCHA": "CAPTCHA graphique",
"Sign In": "Se connecter",
"Register": "S'inscrire",
"E-mail": "E-mail",
@ -55,7 +64,7 @@
"Always loop: ": "Lire en boucle : ",
"Autoplay: ": "Lancer la lecture automatiquement : ",
"Play next by default: ": "Lire les vidéos suivantes par défaut : ",
"Autoplay next video: ": "Lancer la lecture automatiquement pour la vidéo suivant la vidéo regardée : ",
"Autoplay next video: ": "Lire automatiquement la vidéo suivante : ",
"Listen by default: ": "Audio uniquement : ",
"Proxy videos: ": "Charger les vidéos à travers un proxy : ",
"Default speed: ": "Vitesse par défaut : ",
@ -68,6 +77,8 @@
"Fallback captions: ": "Sous-titres alternatifs : ",
"Show related videos: ": "Voir les vidéos liées : ",
"Show annotations by default: ": "Afficher les annotations par défaut : ",
"Automatically extend video description: ": "Etendre automatiquement la description : ",
"Interactive 360 degree videos: ": "Vidéos interactives à 360° : ",
"Visual preferences": "Préférences du site",
"Player style: ": "Style du lecteur : ",
"Dark mode: ": "Mode sombre : ",
@ -75,15 +86,17 @@
"dark": "sombre",
"light": "clair",
"Thin mode: ": "Mode léger : ",
"Subscription preferences": "Préférences de la page d'abonnements",
"Miscellaneous preferences": "Paramètres divers",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Redirection vers une autre instance automatique (via redirect.invidious.io) : ",
"Subscription preferences": "Préférences des abonnements",
"Show annotations by default for subscribed channels: ": "Afficher les annotations par défaut sur les chaînes auxquelles vous êtes abonnés : ",
"Redirect homepage to feed: ": "Rediriger la page d'accueil vers la page d'abonnements : ",
"Number of videos shown in feed: ": "Nombre de vidéos affichées dans la page d'abonnements : ",
"Sort videos by: ": "Trier les vidéos par : ",
"published": "date de publication",
"published - reverse": "date de publication - inversé",
"alphabetically": "alphabétiquement",
"alphabetically - reverse": "alphabétiquement - inversé",
"alphabetically": "ordre alphabétique",
"alphabetically - reverse": "ordre alphabétique - inversé",
"channel name": "nom de la chaîne",
"channel name - reverse": "nom de la chaîne - inversé",
"Only show latest video from channel: ": "Afficher uniquement la dernière vidéo des chaînes auxquelles vous êtes abonnés : ",
@ -91,7 +104,7 @@
"Only show unwatched: ": "Afficher uniquement les vidéos qui n'ont pas été regardées : ",
"Only show notifications (if there are any): ": "Afficher uniquement les notifications (s'il y en a) : ",
"Enable web notifications": "Activer les notifications web",
"`x` uploaded a video": "`x` a partagé(e) une vidéo",
"`x` uploaded a video": "`x` a partagé une vidéo",
"`x` is live": "`x` est en direct",
"Data preferences": "Préférences liées aux données",
"Clear watch history": "Supprimer l'historique des vidéos regardées",
@ -104,25 +117,35 @@
"Administrator preferences": "Préferences d'Administration",
"Default homepage: ": "Page d'accueil par défaut : ",
"Feed menu: ": "Préferences des abonnements : ",
"Show nickname on top: ": "Afficher le nom d'utilisateur en haut à droite : ",
"Top enabled: ": "Top activé : ",
"CAPTCHA enabled: ": "CAPTCHA activé : ",
"Login enabled: ": "Connexion activée : ",
"Registration enabled: ": "Inscription activée : ",
"Report statistics: ": "Télémétrie activé : ",
"Login enabled: ": "Autoriser l'ouverture de sessions utilisateur : ",
"Registration enabled: ": "Autoriser la création de comptes utilisateur : ",
"Report statistics: ": "Activer les statistiques d'instance : ",
"Save preferences": "Enregistrer les préférences",
"Subscription manager": "Gestionnaire d'abonnement",
"Token manager": "Gestionnaire de tokens",
"Token manager": "Gestionnaire de token",
"Token": "Token",
"`x` subscriptions": "`x` abonnements",
"`x` tokens": "`x` tokens",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnements",
"": "`x` abonnements"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` token",
"": "`x` tokens"
},
"Import/export": "Importer/Exporter",
"unsubscribe": "se désabonner",
"revoke": "révoquer",
"Subscriptions": "Abonnements",
"`x` unseen notifications": "`x` notifications non vues",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` notification non vue",
"": "`x` notifications non vues"
},
"search": "rechercher",
"Log out": "Déconnexion",
"Released under the AGPLv3 by Omar Roth.": "Publié sous licence AGPLv3 par Omar Roth.",
"Log out": "Se déconnecter",
"Released under the AGPLv3 on Github.": "Publié sous licence AGPLv3 sur Github.",
"Source available here.": "Code source disponible ici.",
"View JavaScript license information.": "Informations des licences JavaScript.",
"View privacy policy.": "Politique de confidentialité.",
@ -138,7 +161,11 @@
"Title": "Titre",
"Playlist privacy": "Paramètres de confidentialité de la liste de lecture",
"Editing playlist `x`": "Liste de lecture modifier le `x`",
"Show more": "Afficher plus",
"Show less": "Afficher moins",
"Watch on YouTube": "Voir la vidéo sur Youtube",
"Switch Invidious Instance": "Changer d'instance",
"Broken? Try another Invidious Instance": "Instance Invidious défectueuse ? Essayez-en une autre",
"Hide annotations": "Masquer les annotations",
"Show annotations": "Afficher les annotations",
"Genre: ": "Genre : ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Régions sur liste blanche : ",
"Blacklisted regions: ": "Régions sur liste noire : ",
"Shared `x`": "Ajoutée le `x`",
"`x` views": "`x` vues",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` vues",
"": "`x` vues"
},
"Premieres in `x`": "Première dans `x`",
"Premieres `x`": "Première le `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.": "Il semblerait que JavaScript soit désactivé. Cliquez ici pour voir les commentaires, mais gardez à l'esprit que le chargement peut prendre plus de temps.",
"View YouTube comments": "Voir les commentaires YouTube",
"View more comments on Reddit": "Voir plus de commentaires sur Reddit",
"View `x` comments": "Voir `x` commentaires",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Voir `x` commentaire",
"": "Voir `x` commentaires"
},
"View Reddit comments": "Voir les commentaires Reddit",
"Hide replies": "Masquer les réponses",
"Show replies": "Afficher les réponses",
@ -180,10 +213,16 @@
"This channel does not exist.": "Cette chaine n'existe pas.",
"Could not get channel info.": "Impossible de charger les informations de cette chaîne.",
"Could not fetch comments": "Impossible de charger les commentaires",
"View `x` replies": "Voir `x` réponses",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Voir `x` réponse",
"": "Voir `x` réponses"
},
"`x` ago": "il y a `x`",
"Load more": "Voir plus",
"`x` points": "`x` points",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` point",
"": "`x` points"
},
"Could not create mix.": "Impossible de charger cette liste de lecture.",
"Empty playlist": "La liste de lecture est vide",
"Not a playlist.": "La liste de lecture est invalide.",
@ -301,15 +340,37 @@
"Yiddish": "Yiddish",
"Yoruba": "Yoruba",
"Zulu": "Zoulou",
"`x` years": "`x` ans",
"`x` months": "`x` mois",
"`x` weeks": "`x` semaines",
"`x` days": "`x` jours",
"`x` hours": "`x` heures",
"`x` minutes": "`x` minutes",
"`x` seconds": "`x` secondes",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` an",
"": "`x` ans"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` mois",
"": "`x` mois"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` semaine",
"": "`x` semaines"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` jour",
"": "`x` jours"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` heure",
"": "`x` heures"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minute",
"": "`x` minutes"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` seconde",
"": "`x` secondes"
},
"Fallback comments: ": "Commentaires alternatifs : ",
"Popular": "Populaire",
"Search": "Rechercher",
"Top": "Top",
"About": "À propos",
"Rating: ": "Évaluation : ",
@ -332,5 +393,35 @@
"Videos": "Vidéos",
"Playlists": "Listes de lecture",
"Community": "Communauté",
"Current version: ": "Version actuelle : "
"relevance": "pertinence",
"rating": "évaluation",
"date": "date",
"views": "nombre de vues",
"content_type": "type",
"duration": "durée",
"features": "fonctionnalités",
"sort": "Trier par",
"hour": "dernière heure",
"today": "aujourd'hui",
"week": "semaine",
"month": "mois",
"year": "année",
"video": "vidéo",
"channel": "chaîne",
"playlist": "liste de lecture",
"movie": "film",
"show": "émission",
"hd": "HD",
"subtitles": "sous-titres / CC",
"creative_commons": "Creative Commons",
"3d": "3D",
"live": "en direct",
"4k": "4K",
"location": "emplacement",
"hdr": "HDR",
"filter": "filtrer",
"Current version: ": "Version actuelle : ",
"next_steps_error_message": "Vous pouvez essayer de : ",
"next_steps_error_message_refresh": "Rafraîchir la page",
"next_steps_error_message_go_to_youtube": "Aller sur YouTube"
}

View File

@ -1,25 +1,25 @@
{
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` רשומים.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` רשומים."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` רשומים",
"": "`x` רשומים"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` סרטונים.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` סרטונים."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` סרטונים",
"": "`x` סרטונים"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` פלייליסטים.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` פלייליסטים."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` פלייליסטים",
"": "`x` פלייליסטים"
},
"LIVE": "שידור חי",
"Shared `x` ago": "",
"Shared `x` ago": "שותף לפני `x`",
"Unsubscribe": "ביטול מינוי",
"Subscribe": "הרשמה למינוי",
"View channel on YouTube": "צפייה בערוץ ב־YouTube",
"View playlist on YouTube": "צפייה בפלייליסט ב־YouTube",
"newest": "החדש ביותר",
"oldest": "הישן ביותר",
"popular": "פופולארי",
"popular": "סרטונים פופולריים",
"last": "אחרון",
"Next page": "העמוד הבא",
"Previous page": "העמוד הקודם",
@ -27,8 +27,8 @@
"New password": "סיסמה חדשה",
"New passwords must match": "על הסיסמאות החדשות להתאים",
"Cannot change password for Google accounts": "לא ניתן לשנות את הסיסמה לחשבונות Google",
"Authorize token?": "",
"Authorize token for `x`?": "",
"Authorize token?": "לאשר את האסימון?",
"Authorize token for `x`?": "האם לאשר את האסימון עבור `x`?",
"Yes": "כן",
"No": "לא",
"Import and Export Data": "ייבוא וייצוא נתונים",
@ -45,7 +45,7 @@
"Delete account?": "למחוק את החשבון?",
"History": "היסטוריה",
"An alternative front-end to YouTube": "ממשק משתמש חלופי ל־YouTube",
"JavaScript license information": "",
"JavaScript license information": "מידע על רישיון JavaScript",
"source": "source",
"Log in": "כניסה",
"Log in/register": "כניסה/הרשמה",
@ -63,20 +63,22 @@
"Player preferences": "העדפות הנגן",
"Always loop: ": "",
"Autoplay: ": "ניגון אוטומטי: ",
"Play next by default: ": "",
"Autoplay next video: ": גן אוטומטית את הסרטון הבא ",
"Listen by default: ": "האזן כברירת מחדל: ",
"Play next by default: ": "ניגון הסרטון הבא כברירת מחדל: ",
"Autoplay next video: ": יגון הסרטון הבא באופן אוטומטי: ",
"Listen by default: ": "שמע כברירת מחדל: ",
"Proxy videos: ": "",
"Default speed: ": "מהירות ברירת המחדל: ",
"Preferred video quality: ": "איכות הווידאו המועדפת: ",
"Player volume: ": וצמת שמע בנגן: ",
"Player volume: ": צמת השמע של הנגן: ",
"Default comments: ": "תגובות ברירת מחדל ",
"youtube": "יוטיוב",
"reddit": "reddit",
"Default captions: ": "כתוביות ברירת מחדל ",
"Fallback captions: ": "כתוביות גיבוי ",
"Show related videos: ": "הראה סרטונים קשורים: ",
"Show annotations by default: ": "הראה הסברים כברירת מחדל: ",
"Show related videos: ": "הצגת סרטונים קשורים: ",
"Show annotations by default: ": "הצגת הערות כברירת מחדל: ",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "העדפות חזותיות",
"Player style: ": "סגנון הנגן: ",
"Dark mode: ": "מצב כהה: ",
@ -84,6 +86,8 @@
"dark": "כהה",
"light": "בהיר",
"Thin mode: ": "",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "העדפות מינויים",
"Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ",
"Redirect homepage to feed: ": "",
@ -95,10 +99,10 @@
"alphabetically - reverse": "בסדר אלפביתי - הפוך",
"channel name": "שם הערוץ",
"channel name - reverse": "שם הערוץ - הפוך",
"Only show latest video from channel: ": ראה רק את הסרטון האחרון מהערוץ: ",
"Only show latest unwatched video from channel: ": ראה רק את הסרטון האחרון שלא נצפה מהערוץ: ",
"Only show unwatched: ": ראה רק סרטונים שלא נצפו ",
"Only show notifications (if there are any): ": ראה רק התראות (אם יש) ",
"Only show latest video from channel: ": צגת הסרטון האחרון מהערוץ בלבד: ",
"Only show latest unwatched video from channel: ": צגת הסרטון האחרון שלא נצפה מהערוץ בלבד: ",
"Only show unwatched: ": צגת סרטונים שלא נצפו בלבד: ",
"Only show notifications (if there are any): ": צגת התראות בלבד (אם ישנן): ",
"Enable web notifications": "",
"`x` uploaded a video": "סרטון הועלה על ידי `x`",
"`x` is live": "`x` בשידור חי",
@ -107,12 +111,13 @@
"Import/export data": "ייבוא/ייצוא נתונים",
"Change password": "שינוי הסיסמה",
"Manage subscriptions": "ניהול מינויים",
"Manage tokens": "",
"Manage tokens": "ניהול אסימונים",
"Watch history": "היסטוריית צפייה",
"Delete account": "מחיקת החשבון",
"Administrator preferences": "",
"Administrator preferences": "הגדרות ניהול מערכת",
"Default homepage: ": "Default homepage: ",
"Feed menu: ": "תפריט ההזנה: ",
"Show nickname on top: ": "",
"Top enabled: ": "",
"CAPTCHA enabled: ": "",
"Login enabled: ": "",
@ -123,8 +128,8 @@
"Token manager": "Token manager",
"Token": "Token",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` מינויים.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` מינויים."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` מינויים",
"": "`x` מינויים"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
@ -135,12 +140,12 @@
"revoke": "",
"Subscriptions": "מינויים",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` הודעות שלא נראו.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` הודעות שלא נראו."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` הודעות שלא נראו",
"": "`x` הודעות שלא נראו"
},
"search": "חיפוש",
"Log out": "יציאה",
"Released under the AGPLv3 by Omar Roth.": "מופץ תחת רישיון AGPLv3 על ידי עמר רות׳ (Omar Roth).",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "קוד המקור זמין כאן.",
"View JavaScript license information.": "",
"View privacy policy.": "להצגת מדיניות הפרטיות.",
@ -156,31 +161,35 @@
"Title": "",
"Playlist privacy": "Playlist privacy",
"Editing playlist `x`": "",
"Show more": "",
"Show less": "",
"Watch on YouTube": "צפייה ב־YouTube",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "",
"Show annotations": "",
"Genre: ": "Genre: ",
"License: ": "רישיון: ",
"Family friendly? ": "לכל המשפחה? ",
"Wilson score: ": "",
"Wilson score: ": "ציון וילסון: ",
"Engagement: ": "",
"Whitelisted regions: ": "",
"Blacklisted regions: ": "",
"Shared `x`": "",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` צפיות.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` צפיות."
"": "`x` צפיות"
},
"Premieres in `x`": "",
"Premieres `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.": "שלום! נראה ש־JavaScript כבוי. יש ללחוץ כאן להצגת התגובות, נא לקחת בחשבון שהטעינה תיקח קצת יותר זמן.",
"View YouTube comments": "",
"View more comments on Reddit": "",
"View YouTube comments": "הצגת התגובות מ־YouTube",
"View more comments on Reddit": "להצגת תגובות נוספות ב־Reddit",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "הצגת `x` תגובות.([^.,0-9]|^)1([^.,0-9]|$)",
"": "הצגת `x` תגובות."
"": "הצגת `x` תגובות"
},
"View Reddit comments": "",
"View Reddit comments": "להצגת התגובות ב־Reddit",
"Hide replies": "הסתרת תגובות",
"Show replies": "הצגת תגובות",
"Incorrect password": "סיסמה שגויה",
@ -190,23 +199,23 @@
"Login failed. This may be because two-factor authentication is not turned on for your account.": "",
"Wrong answer": "תשובה שגויה",
"Erroneous CAPTCHA": "",
"CAPTCHA is a required field": "",
"CAPTCHA is a required field": "שדה CAPTCHA הוא שדה חובה",
"User ID is a required field": "חובה למלא את שדה שם המשתמש",
"Password is a required field": "חובה למלא את שדה הסיסמה",
"Wrong username or password": "שם משתמש שגוי או סיסמה שגויה",
"Please sign in using 'Log in with Google'": "",
"Please sign in using 'Log in with Google'": "נא להתחבר בעזרת \"התחברות עם Google\"",
"Password cannot be empty": "",
"Password cannot be longer than 55 characters": "",
"Password cannot be longer than 55 characters": "על אורך הסיסמה להיות 55 תווים לכל היותר",
"Please log in": "נא להתחבר",
"Invidious Private Feed for `x`": "",
"channel:`x`": "ערוץ:`x`",
"Deleted or invalid channel": "",
"Deleted or invalid channel": "הערוץ נמחק או שאינו תקין",
"This channel does not exist.": "הערוץ הזה אינו קיים.",
"Could not get channel info.": "לא היה ניתן לקבל מידע על הערוץ.",
"Could not fetch comments": "לא היה ניתן למשוך את התגובות",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "הצגת `x` תגובות.([^.,0-9]|^)1([^.,0-9]|$)",
"": "הצגת `x` תגובות."
"([^.,0-9]|^)1([^.,0-9]|$)": "הצגת `x` תגובות",
"": "הצגת `x` תגובות"
},
"`x` ago": "לפני `x`",
"Load more": "לטעון עוד",
@ -224,7 +233,7 @@
"Erroneous challenge": "",
"Erroneous token": "",
"No such user": "אין משתמש כזה",
"Token is expired, please try again": "",
"Token is expired, please try again": "תוקף האסימון פג, נא לנסות שוב",
"English": "אנגלית",
"English (auto-generated)": "אנגלית (נוצר באופן אוטומטי)",
"Afrikaans": "Afrikaans",
@ -263,10 +272,10 @@
"Hawaiian": "הוואית",
"Hebrew": "עברית",
"Hindi": "הינדית",
"Hmong": "",
"Hmong": "המונג",
"Hungarian": "הונגרית",
"Icelandic": "איסלנדית",
"Igbo": "",
"Igbo": "איגבו",
"Indonesian": "אינדונזית",
"Irish": "Irish",
"Italian": "איטלקית",
@ -286,7 +295,7 @@
"Macedonian": "מקדונית",
"Malagasy": "מלגשית",
"Malay": "מלאית",
"Malayalam": "",
"Malayalam": "מלאיאלאם",
"Maltese": "מלטזית",
"Maori": "מאורית",
"Marathi": "מראטהית",
@ -301,7 +310,7 @@
"Punjabi": "פנג'אבי",
"Romanian": "רומנית",
"Russian": "רוסית",
"Samoan": "",
"Samoan": "סמואית",
"Scottish Gaelic": "גאלית סקוטית",
"Serbian": "Serbian",
"Shona": "",
@ -329,38 +338,39 @@
"Western Frisian": "",
"Xhosa": "קוסה",
"Yiddish": "יידיש",
"Yoruba": "",
"Yoruba": "יורובה",
"Zulu": "זולו",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שנים.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` שנים."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שנים",
"": "`x` שנים"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` חודשים.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` חודשים."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` חודשים",
"": "`x` חודשים"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שבועות.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` שבועות."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שבועות",
"": "`x` שבועות"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ימים.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` ימים."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ימים",
"": "`x` ימים"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שעות.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` שעות."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שעות",
"": "`x` שעות"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` דקות.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` דקות."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` דקות",
"": "`x` דקות"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שניות.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` שניות."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שניות",
"": "`x` שניות"
},
"Fallback comments: ": "",
"Popular": "",
"Popular": "סרטונים פופולריים",
"Search": "",
"Top": "Top",
"About": "על אודות",
"Rating: ": "דירוג: ",
@ -374,7 +384,7 @@
"Download": "הורדה",
"Download as: ": "הורדה בתור: ",
"%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "",
"(edited)": "(לאחר עריכה)",
"YouTube comment permalink": "",
"permalink": "",
"`x` marked it with a ❤": "סומנה ב־❤ על ידי `x`",
@ -410,5 +420,8 @@
"location": "מיקום",
"hdr": "HDR",
"filter": "סינון",
"Current version: ": "הגרסה הנוכחית: "
"Current version: ": "הגרסה הנוכחית: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,15 +1,15 @@
{
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pretplatnika.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` pretplatnika."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pretplatnika",
"": "`x` pretplatnika"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videa.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` videa."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videa",
"": "`x` videa"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` playliste.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` playliste."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` playliste",
"": "`x` playliste"
},
"LIVE": "UŽIVO",
"Shared `x` ago": "Dijeljeno prije `x`",
@ -71,12 +71,14 @@
"Preferred video quality: ": "Primarna kvaliteta videa: ",
"Player volume: ": "Glasnoća playera: ",
"Default comments: ": "Standardni komentari: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Standardni titlovi: ",
"Fallback captions: ": "Alternativni titlovi: ",
"Show related videos: ": "Prikaži povezana videa: ",
"Show annotations by default: ": "Standardno prikaži napomene: ",
"Automatically extend video description: ": "Automatski proširi opis videa: ",
"Interactive 360 degree videos: ": "Interaktivna videa od 360 stupnjeva: ",
"Visual preferences": "Postavke prikaza",
"Player style: ": "Stil playera: ",
"Dark mode: ": "Tamni modus: ",
@ -84,6 +86,8 @@
"dark": "tamno",
"light": "svijetlo",
"Thin mode: ": "Pojednostavljen prikaz: ",
"Miscellaneous preferences": "Razne postavke",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Automatsko preusmjeravanje instance (u krajnjem slučaju koristi redirect.invidious.io): ",
"Subscription preferences": "Postavke pretplata",
"Show annotations by default for subscribed channels: ": "Standardno prikaži napomene za pretplaćene kanale: ",
"Redirect homepage to feed: ": "Preusmjeri početnu stranicu na feed: ",
@ -113,6 +117,7 @@
"Administrator preferences": "Postavke administratora",
"Default homepage: ": "Standardna početna stranica: ",
"Feed menu: ": "Izbornik za feedove: ",
"Show nickname on top: ": "Prikaži nadimak na vrhu: ",
"Top enabled: ": "Najbolji aktivirani: ",
"CAPTCHA enabled: ": "Aktivirani CAPTCHA: ",
"Login enabled: ": "Prijava aktivirana: ",
@ -123,24 +128,24 @@
"Token manager": "Upravljanje tokenima",
"Token": "Token",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pretplate.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` pretplate."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pretplate",
"": "`x` pretplate"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokena.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` tokena."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokena",
"": "`x` tokena"
},
"Import/export": "Uvezi/izvezi",
"unsubscribe": "odjavi pretplatu",
"revoke": "opozovi",
"Subscriptions": "Pretplate",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` neviđene obavijesti.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` neviđene obavijesti."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` neviđene obavijesti",
"": "`x` neviđene obavijesti"
},
"search": "traži",
"Log out": "Odjavi se",
"Released under the AGPLv3 by Omar Roth.": "Izdano pod licencom AGPLv3, Omar Roth.",
"Released under the AGPLv3 on Github.": "Izdano pod licencom AGPLv3 na Github-u.",
"Source available here.": "Izvor je ovdje dostupan.",
"View JavaScript license information.": "Prikaži informacije o JavaScript licenci.",
"View privacy policy.": "Prikaži politiku privatnosti.",
@ -156,7 +161,11 @@
"Title": "Naslov",
"Playlist privacy": "Privatnost playliste",
"Editing playlist `x`": "Uređivanje playliste `x`",
"Show more": "Pokaži više",
"Show less": "Pokaži manje",
"Watch on YouTube": "Gledaj na YouTubeu",
"Switch Invidious Instance": "Promijeni Invidious instancu",
"Broken? Try another Invidious Instance": "Pokvarena? Probaj jednu drugu Invidious instancu",
"Hide annotations": "Sakrij napomene",
"Show annotations": "Prikaži napomene",
"Genre: ": "Žanr: ",
@ -169,7 +178,7 @@
"Shared `x`": "Dijeljeno `x`",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` gledanja.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` gledanja."
"": "`x` gledanja"
},
"Premieres in `x`": "Premijera za `x`",
"Premieres `x`": "Premijera `x`",
@ -178,7 +187,7 @@
"View more comments on Reddit": "Prikaži još komentara na Redditu",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Prikaži `x` komentara.([^.,0-9]|^)1([^.,0-9]|$)",
"": "Prikaži `x` komentara."
"": "Prikaži `x` komentara"
},
"View Reddit comments": "Prikaži Reddit komentare",
"Hide replies": "Sakrij odgovore",
@ -205,14 +214,14 @@
"Could not get channel info.": "Neuspjelo dobivanje podataka kanala.",
"Could not fetch comments": "Neuspjelo dohvaćanje komentara",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Prikaži `x` odgovora.([^.,0-9]|^)1([^.,0-9]|$)",
"": "Prikaži `x` odgovora."
"([^.,0-9]|^)1([^.,0-9]|$)": "Prikaži `x` odgovora",
"": "Prikaži `x` odgovora"
},
"`x` ago": "prije `x`",
"Load more": "Učitaj više",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` bodova.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` bodova."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` bodova",
"": "`x` bodova"
},
"Could not create mix.": "Neuspjelo stvaranje miksa.",
"Empty playlist": "Prazna playlista",
@ -332,35 +341,36 @@
"Yoruba": "Jorubški",
"Zulu": "Zulu",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` g.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` g."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` g",
"": "`x` g"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` mj.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` mj."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` mj",
"": "`x` mj"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tj.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` tj."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tj",
"": "`x` tj"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dana.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` dana."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dana",
"": "`x` dana"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` h.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` h."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` h",
"": "`x` h"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` min.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` min."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` min",
"": "`x` min"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` s.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` s."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` s",
"": "`x` s"
},
"Fallback comments: ": "Alternativni komentari: ",
"Popular": "Popularni",
"Search": "Traži",
"Top": "Najbolji",
"About": "Informacije",
"Rating: ": "Ocjena: ",
@ -375,13 +385,43 @@
"Download as: ": "Preuzmi kao: ",
"%A %B %-d, %Y": "%A, %-d. %B %Y.",
"(edited)": "(uređeno)",
"YouTube comment permalink": "Permalink YouTube komentara",
"permalink": "permalink",
"YouTube comment permalink": "Stalna poveznica YouTube komentara",
"permalink": "stalna poveznica",
"`x` marked it with a ❤": "Označeno sa ❤ od `x`",
"Audio mode": "Audio modus",
"Video mode": "Videomodus",
"Videos": "Videa",
"Playlists": "Playliste",
"Community": "Zajednica",
"Current version: ": "Trenutačna verzija: "
"relevance": "značaj",
"rating": "ocjena",
"date": "datum",
"views": "prikazi",
"content_type": "vrsta_sadržaja",
"duration": "trajanje",
"features": "funkcije",
"sort": "redoslijed",
"hour": "sat",
"today": "danas",
"week": "tjedan",
"month": "mjesec",
"year": "godina",
"video": "video",
"channel": "kanal",
"playlist": "playlista",
"movie": "film",
"show": "emisija",
"hd": "hd",
"subtitles": "titlovi",
"creative_commons": "creative_commons",
"3d": "3d",
"live": "uživo",
"4k": "4k",
"location": "lokacija",
"hdr": "hdr",
"filter": "filtar",
"Current version: ": "Trenutačna verzija: ",
"next_steps_error_message": "Nakon toga pokušaj sljedeće: ",
"next_steps_error_message_refresh": "Aktualiziraj stranicu",
"next_steps_error_message_go_to_youtube": "Idi na YouTube"
}

View File

@ -1,13 +1,22 @@
{
"`x` subscribers": "`x` feliratkozó",
"`x` videos": "`x` videó",
"`x` playlists": "`x` playlist",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` feliratkozó"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` videó"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` playlist"
},
"LIVE": "ÉLŐ",
"Shared `x` ago": "`x` óta megosztva",
"Unsubscribe": "Leiratkozás",
"Subscribe": "Feliratkozás",
"View channel on YouTube": "Csatokrna megtekintése a YouTube-on",
"View playlist on YouTube": "Playlist megtekintése a YouTube-on",
"View channel on YouTube": "csatorna megtekintése a YouTube-on",
"View playlist on YouTube": "lejátszási lista megtekintése a YouTube-on",
"newest": "legújabb",
"oldest": "legrégibb",
"popular": "népszerű",
@ -17,7 +26,7 @@
"Clear watch history?": "Megtekintési napló törlése?",
"New password": "Új jelszó",
"New passwords must match": "Az új jelszavaknak egyezniük kell",
"Cannot change password for Google accounts": "Google fiók jelszavát nem lehet cserélni",
"Cannot change password for Google accounts": "Google fiók jelszavát nem lehet megváltoztatni",
"Authorize token?": "Token felhatalmazása?",
"Authorize token for `x`?": "Token felhatalmazása `x`-ra?",
"Yes": "Igen",
@ -57,35 +66,39 @@
"Play next by default: ": "Következő lejátszása alapértelmezésben: ",
"Autoplay next video: ": "Következő automatikus lejátszása: ",
"Listen by default: ": "Hallgatás alapértelmezésben: ",
"Proxy videos: ": "Proxy videók: ",
"Proxy videos: ": "Videók proxyzása: ",
"Default speed: ": "Alapértelmezett sebesség: ",
"Preferred video quality: ": "Kívánt video minőség: ",
"Player volume: ": "Hangerő: ",
"Default comments: ": "Alapértelmezett kommentek: ",
"youtube": "YouTube",
"reddit": "Reddit",
"reddit": "reddit",
"Default captions: ": "Alapértelmezett feliratok: ",
"Fallback captions: ": "Másodlagos feliratok: ",
"Show related videos: ": "Kapcsolódó videók mutatása: ",
"Show annotations by default: ": "Annotációk mutatása alapértelmetésben: ",
"Visual preferences": "Vizuális preferenciák",
"Show related videos: ": "Hasonló videók mutatása: ",
"Show annotations by default: ": "Szövegmagyarázatok mutatása alapértelmezésben: ",
"Automatically extend video description: ": "Automatikusan növelje meg a videó leírását",
"Interactive 360 degree videos: ": "Interaktív 360° videók",
"Visual preferences": "Kinézeti beállítások",
"Player style: ": "Lejátszó stílusa: ",
"Dark mode: ": "Sötét mód: ",
"Theme: ": "Téma: ",
"dark": "Sötét",
"light": "Világos",
"dark": "sötét",
"light": "világos",
"Thin mode: ": "Vékony mód: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Feliratkozási beállítások",
"Show annotations by default for subscribed channels: ": "Annotációk mutatása alapértelmezésben feliratkozott csatornák esetében: ",
"Show annotations by default for subscribed channels: ": "Szövegmagyarázatok mutatása alapértelmezésben feliratkozott csatornák esetében: ",
"Redirect homepage to feed: ": "Kezdő oldal átirányitása a feed-re: ",
"Number of videos shown in feed: ": "Feed-ben mutatott videók száma: ",
"Sort videos by: ": "Videók sorrendje: ",
"published": "közzétéve",
"published - reverse": "közzétéve (ford.)",
"published - reverse": "közzétéve - fordítva",
"alphabetically": "ABC sorrend",
"alphabetically - reverse": "ABC sorrend (ford.)",
"alphabetically - reverse": "ABC sorrend - fordítva",
"channel name": "csatorna neve",
"channel name - reverse": "csatorna neve (ford.)",
"channel name - reverse": "csatorna neve - fordítva",
"Only show latest video from channel: ": "Csak a legutolsó videó mutatása a csatornából: ",
"Only show latest unwatched video from channel: ": "Csak a legutolsó nem megtekintett videó mutatása a csatornából: ",
"Only show unwatched: ": "Csak a nem megtekintettek mutatása: ",
@ -102,8 +115,9 @@
"Watch history": "Megtekintési napló",
"Delete account": "Fiók törlése",
"Administrator preferences": "Adminisztrátor beállítások",
"Default homepage: ": "Alapértelmezett honlap: ",
"Default homepage: ": "Alapértelmezett oldal: ",
"Feed menu: ": "Feed menü: ",
"Show nickname on top: ": "",
"Top enabled: ": "Top lista engedélyezve: ",
"CAPTCHA enabled: ": "CAPTCHA engedélyezve: ",
"Login enabled: ": "Bejelentkezés engedélyezve: ",
@ -113,56 +127,76 @@
"Subscription manager": "Feliratkozás kezelő",
"Token manager": "Token kezelő",
"Token": "Token",
"`x` subscriptions": "`x` feliratkozás",
"`x` tokens": "`x` token",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` feliratkozás"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` token"
},
"Import/export": "Import/export",
"unsubscribe": "leiratkozás",
"revoke": "visszavonás",
"Subscriptions": "Feliratkozások",
"`x` unseen notifications": "`x` kimaradt érdesítés",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` kimaradt érdesítés"
},
"search": "keresés",
"Log out": "Kijelentkezés",
"Released under the AGPLv3 by Omar Roth.": "Omar Roth által release-elve AGPLv3 licensz alatt.",
"Source available here.": "Forrás elérhető itt.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "A forráskód itt érhető el.",
"View JavaScript license information.": "JavaScript licensz inforkációk megtekintése.",
"View privacy policy.": "Adatvédelem irányelv megtekintése.",
"Trending": "Trending",
"View privacy policy.": "Adatvédelmi irányelvek megtekintése.",
"Trending": "Felkapott",
"Public": "Nyilvános",
"Unlisted": "Nem nyilvános",
"Private": "Privát",
"View all playlists": "Minden playlist megtekintése",
"Updated `x` ago": "Frissitve `x`",
"View all playlists": "Minden lejátszási lista megtekintése",
"Updated `x` ago": "Frissitve: `x`",
"Delete playlist `x`?": "`x` playlist törlése?",
"Delete playlist": "Playlist törlése",
"Create playlist": "Playlist létrehozása",
"Title": "Címe",
"Playlist privacy": "Playlist láthatósága",
"Editing playlist `x`": "`x` playlist szerkesztése",
"Delete playlist": "Lejátszási lista törlése",
"Create playlist": "Lejátszási lista létrehozása",
"Title": "Cím",
"Playlist privacy": "Lejátszási lista láthatósága",
"Editing playlist `x`": "`x` lista szerkesztése",
"Show more": "Mutass többet",
"Show less": "Mutass kevesebbet",
"Watch on YouTube": "Megtekintés a YouTube-on",
"Hide annotations": "Annotációk elrejtése",
"Show annotations": "Annotációk mutatása",
"Genre: ": "Zsáner: ",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "Szövegmagyarázat elrejtése",
"Show annotations": "Szövegmagyarázat mutatása",
"Genre: ": "Műfaj: ",
"License: ": "Licensz: ",
"Family friendly? ": "Családbarát? ",
"Wilson score: ": "Wilson-ponstszém: ",
"Engagement: ": "Engagement: ",
"Wilson score: ": "Wilson-pontszám: ",
"Engagement: ": "elkötelezettség: ",
"Whitelisted regions: ": "Engedélyezett régiók: ",
"Blacklisted regions: ": "Tiltott régiók: ",
"Shared `x`": "Megosztva `x`",
"`x` views": "`x` megtekintés",
"Premieres in `x`": "Premier `x`",
"Premieres `x`": "Premier `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.": "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` megtekintés"
},
"Premieres in `x`": "premierel `x` múlva",
"Premieres `x`": "`x`-t premierel",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Úgy látszik, hogy a JavaScript ki van kapcsolva a böngésződben. Kattints ide hogy megtekintsd a kommenteket, de tudd, hogy így kicsit tovább tarthat a betöltés.",
"View YouTube comments": "YouTube kommentek megtekintése",
"View more comments on Reddit": "További Reddit kommentek megtekintése",
"View `x` comments": "`x` komment megtekintése",
"View more comments on Reddit": "További kommentek megtekintése Redditen",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` komment megtekintése"
},
"View Reddit comments": "Reddit kommentek megtekintése",
"Hide replies": "Válaszok elrejtése",
"Show replies": "Válaszok mutatása",
"Incorrect password": "Helytelen jelszó",
"Quota exceeded, try again in a few hours": "Kvóta túllépve, próbálkozz pár órával később",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Sikertelen belépés, győződj meg róla hogy a 2FA (Authenticator vagy SMS) engedélyezve van.",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Sikertelen belépés, győződj meg róla hogy a 2FA (Authenticator vagy SMS) engedélyezve van.",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Sikertelen bejelentkezés. Győződj meg róla, hogy a kétfaktoros hitelesítés (hitelesítő vagy SMS) engedélyezve van.",
"Invalid TFA code": "",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Sikertelen bejelentkezés. Győződj meg róla, hogy a kétfaktoros hitelesítés engedélyezve van.",
"Wrong answer": "Rossz válasz",
"Erroneous CAPTCHA": "Hibás CAPTCHA",
"CAPTCHA is a required field": "A CAPTCHA kötelező",
@ -171,149 +205,177 @@
"Wrong username or password": "Rossz felhasználónév vagy jelszó",
"Please sign in using 'Log in with Google'": "Kérem, jelentkezzen be a \"Bejelentkezés Google-el\"",
"Password cannot be empty": "A jelszó nem lehet üres",
"Password cannot be longer than 55 characters": "A jelszó nem lehet hosszabb 55 betűnél",
"Password cannot be longer than 55 characters": "A jelszó nem lehet hosszabb 55 karakternél",
"Please log in": "Kérem lépjen be",
"Invidious Private Feed for `x`": "`x` Invidious privát feed-je",
"channel:`x`": "`x` csatorna",
"Deleted or invalid channel": "Törölt vagy nemlétező csatorna",
"This channel does not exist.": "Ez a csatorna nem létezik.",
"Could not get channel info.": "Nem megszerezhető a csatorna információ.",
"Could not fetch comments": "Nem megszerezhetőek a kommentek",
"View `x` replies": "`x` válasz megtekintése",
"Could not get channel info.": "Nem sikerült lekérni a csatorna adatokat.",
"Could not fetch comments": "Nem sikerült lekérni a kommenteket",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` válasz megtekintése"
},
"`x` ago": "`x` óta",
"Load more": "További betöltése",
"`x` points": "`x` pont",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` pont"
},
"Could not create mix.": "Nem tudok mix-et készíteni.",
"Empty playlist": "Üres playlist",
"Not a playlist.": "Nem playlist.",
"Playlist does not exist.": "Nem létező playlist.",
"Could not pull trending pages.": "Nem tudom letölteni a trendek adatait.",
"Empty playlist": "Üres lejátszási lista",
"Not a playlist.": "Nem lejátszási lista.",
"Playlist does not exist.": "Nincs ilyen lejátszási lista.",
"Could not pull trending pages.": "Nem sikerült lekérni a felkapott oldalt.",
"Hidden field \"challenge\" is a required field": "A rejtett \"challenge\" mező kötelező",
"Hidden field \"token\" is a required field": "A rejtett \"token\" mező kötelező",
"Erroneous challenge": "Hibás challenge",
"Erroneous token": "Hibás token",
"No such user": "Nincs ilyen felhasználó",
"Token is expired, please try again": "Lejárt token, kérem próbáld újra",
"English": "",
"English (auto-generated)": "English (auto-genererat)",
"Afrikaans": "",
"Albanian": "",
"Amharic": "",
"Arabic": "",
"Armenian": "",
"Azerbaijani": "",
"Bangla": "",
"Basque": "",
"Belarusian": "",
"Bosnian": "",
"Bulgarian": "",
"Burmese": "",
"Catalan": "",
"Cebuano": "",
"Chinese (Simplified)": "",
"Chinese (Traditional)": "",
"Corsican": "",
"Croatian": "",
"Czech": "",
"Danish": "",
"Dutch": "",
"Esperanto": "",
"Estonian": "",
"Filipino": "",
"Finnish": "",
"French": "",
"Galician": "",
"Georgian": "",
"German": "",
"Greek": "",
"Gujarati": "",
"Haitian Creole": "",
"Hausa": "",
"Hawaiian": "",
"Hebrew": "",
"Hindi": "",
"Hmong": "",
"Hungarian": "",
"Icelandic": "",
"Igbo": "",
"Indonesian": "",
"Irish": "",
"Italian": "",
"Japanese": "",
"Javanese": "",
"Kannada": "",
"Kazakh": "",
"Khmer": "",
"Korean": "",
"Kurdish": "",
"Kyrgyz": "",
"Lao": "",
"Latin": "",
"Latvian": "",
"Lithuanian": "",
"Luxembourgish": "",
"Macedonian": "",
"Malagasy": "",
"Malay": "",
"Malayalam": "",
"Maltese": "",
"Maori": "",
"Marathi": "",
"Mongolian": "",
"Nepali": "",
"Norwegian Bokmål": "",
"Nyanja": "",
"Pashto": "",
"Persian": "",
"Polish": "",
"Portuguese": "",
"Punjabi": "",
"Romanian": "",
"Russian": "",
"Samoan": "",
"Scottish Gaelic": "",
"Serbian": "",
"Shona": "",
"Sindhi": "",
"Sinhala": "",
"Slovak": "",
"Slovenian": "",
"Somali": "",
"Southern Sotho": "",
"Spanish": "",
"Spanish (Latin America)": "",
"Sundanese": "",
"Swahili": "",
"Swedish": "",
"Tajik": "",
"Tamil": "",
"Telugu": "",
"Thai": "",
"Turkish": "",
"Ukrainian": "",
"English": "angol",
"English (auto-generated)": "angol (automatikusan generált)",
"Afrikaans": "afrikaans",
"Albanian": "albán",
"Amharic": "amhara",
"Arabic": "arab",
"Armenian": "örmény",
"Azerbaijani": "azerbajdzsáni",
"Bangla": "bengáli",
"Basque": "baszk",
"Belarusian": "fehérorosz",
"Bosnian": "bosnyák",
"Bulgarian": "bolgár",
"Burmese": "burmai",
"Catalan": "katalán",
"Cebuano": "szebuano",
"Chinese (Simplified)": "kínai (egyszerűsített)",
"Chinese (Traditional)": "kínai (hagyományos)",
"Corsican": "korzikai",
"Croatian": "horvát",
"Czech": "cseh",
"Danish": "dán",
"Dutch": "holland",
"Esperanto": "eszperantó",
"Estonian": "észt",
"Filipino": "filippínó",
"Finnish": "finn",
"French": "francia",
"Galician": "galíciai",
"Georgian": "grúz",
"German": "német",
"Greek": "görök",
"Gujarati": "gudzsaráti",
"Haitian Creole": "haiti kreol",
"Hausa": "hausza",
"Hawaiian": "hawaii",
"Hebrew": "héber",
"Hindi": "hindi",
"Hmong": "hmong",
"Hungarian": "magyar",
"Icelandic": "izlandi",
"Igbo": "igbo",
"Indonesian": "indonéziai",
"Irish": "ír",
"Italian": "olasz",
"Japanese": "japán",
"Javanese": "jávai",
"Kannada": "kannada",
"Kazakh": "kazah",
"Khmer": "khmer",
"Korean": "koreai",
"Kurdish": "kurd",
"Kyrgyz": "kirgiz",
"Lao": "lao",
"Latin": "latin",
"Latvian": "lett",
"Lithuanian": "litván",
"Luxembourgish": "luxemburgi",
"Macedonian": "macedóniai",
"Malagasy": "madagaszkári",
"Malay": "maláj",
"Malayalam": "malajálam",
"Maltese": "máltai",
"Maori": "maori",
"Marathi": "Maráthi",
"Mongolian": "mongol",
"Nepali": "nepáli",
"Norwegian Bokmål": "bokmål",
"Nyanja": "nyánja",
"Pashto": "pastu",
"Persian": "perzsa",
"Polish": "lengyel",
"Portuguese": "portugál",
"Punjabi": "pandzsábi",
"Romanian": "román",
"Russian": "orosz",
"Samoan": "szamoai",
"Scottish Gaelic": "skót gael",
"Serbian": "szerb",
"Shona": "shona",
"Sindhi": "szindhi",
"Sinhala": "szingaléz",
"Slovak": "szlovák",
"Slovenian": "szlovén",
"Somali": "szomáliai",
"Southern Sotho": "déli szothó",
"Spanish": "spanyol",
"Spanish (Latin America)": "spanyol (Latin-Amerika)",
"Sundanese": "szunda",
"Swahili": "szuahéli",
"Swedish": "svld",
"Tajik": "tadzsik",
"Tamil": "tamil",
"Telugu": "telugu",
"Thai": "thai",
"Turkish": "török",
"Ukrainian": "ukrán",
"Urdu": "",
"Uzbek": "",
"Vietnamese": "",
"Welsh": "",
"Western Frisian": "",
"Uzbek": "üzbég",
"Vietnamese": "vietnámi",
"Welsh": "walesi",
"Western Frisian": "nyugati fríz",
"Xhosa": "",
"Yiddish": "",
"Yoruba": "",
"Zulu": "",
"`x` years": "`x` év",
"`x` months": "`x` hónap",
"`x` weeks": "`x` hét",
"`x` days": "`x` nap",
"`x` hours": "`x` óra",
"`x` minutes": "`x` perc",
"`x` seconds": "`x` másodperc",
"Yiddish": "jiddis",
"Yoruba": "joruba",
"Zulu": "zulu",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` év"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` hónap"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` hét"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` nap"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` óra"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` perc"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` másodperc"
},
"Fallback comments: ": "Másodlagos kommentek: ",
"Popular": "Népszerű",
"Search": "Keresés",
"Top": "Top",
"About": "Leírás",
"Rating: ": "Besorolás: ",
"Language: ": "Nyelv: ",
"View as playlist": "Megtekintés playlist-ként",
"View as playlist": "Megtekintés lejátszási listaként",
"Default": "Alapértelmezett",
"Music": "Zene",
"Gaming": "Játékok",
@ -326,10 +388,40 @@
"YouTube comment permalink": "YouTube komment permalink",
"permalink": "permalink",
"`x` marked it with a ❤": "`x` jelölte ❤-vel",
"Audio mode": "Audio mód",
"Video mode": "Video mód",
"Audio mode": "Audió mód",
"Video mode": "Hang mód",
"Videos": "Videók",
"Playlists": "Playlistek",
"Playlists": "Lejátszási listák",
"Community": "Közösség",
"Current version: ": "Jelenlegi verzió: "
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "Jelenlegi verzió: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,15 +1,15 @@
{
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pelanggan.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` pelanggan."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pelanggan",
"": "`x` pelanggan"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` video.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` video."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` video",
"": "`x` video"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` daftar putar.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` daftar putar."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` daftar putar",
"": "`x` daftar putar"
},
"LIVE": "SIARAN LANGSUNG",
"Shared `x` ago": "Dibagikan`x` lalu",
@ -71,12 +71,14 @@
"Preferred video quality: ": "Kualitas video yang disukai: ",
"Player volume: ": "Volume pemutar: ",
"Default comments: ": "Komentar default: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Subtitel default: ",
"Fallback captions: ": "Subtitel fallback: ",
"Show related videos: ": "Tampilkan video terkait: ",
"Show annotations by default: ": "Tampilkan anotasi secara default: ",
"Automatically extend video description: ": "Perluas deskripsi video secara otomatis: ",
"Interactive 360 degree videos: ": "Video interaktif 360°: ",
"Visual preferences": "Preferensi visual",
"Player style: ": "Gaya pemutar: ",
"Dark mode: ": "Mode gelap: ",
@ -84,6 +86,8 @@
"dark": "gelap",
"light": "terang",
"Thin mode: ": "Mode tipis: ",
"Miscellaneous preferences": "Preferensi lainnya",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Pengalihan instans otomatis (fallback ke redirect.invidious.io): ",
"Subscription preferences": "Preferensi langganan",
"Show annotations by default for subscribed channels: ": "Tampilkan anotasi secara default untuk kanal langganan: ",
"Redirect homepage to feed: ": "Arahkan kembali laman beranda ke umpan: ",
@ -113,7 +117,8 @@
"Administrator preferences": "Preferensi administrator",
"Default homepage: ": "Laman beranda default: ",
"Feed menu: ": "Menu umpan: ",
"Top enabled: ": "",
"Show nickname on top: ": "Tampilkan nama panggilan di atas: ",
"Top enabled: ": "Teratas diaktifkan: ",
"CAPTCHA enabled: ": "CAPTCHA diaktifkan: ",
"Login enabled: ": "Masuk diaktifkan: ",
"Registration enabled: ": "Registrasi diaktifkan: ",
@ -123,24 +128,24 @@
"Token manager": "Pengatur token",
"Token": "Token",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` langganan.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` langganan."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` langganan",
"": "`x` langganan"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` token.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` token."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` token",
"": "`x` token"
},
"Import/export": "Impor/ekspor",
"unsubscribe": "batal langganan",
"revoke": "cabut",
"Subscriptions": "Langganan",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pemberitahuan belum dilihat.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` pemberitahuan belum dilihat."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pemberitahuan belum dilihat",
"": "`x` pemberitahuan belum dilihat"
},
"search": "cari",
"Log out": "Keluar",
"Released under the AGPLv3 by Omar Roth.": "Dirilis dibawah AGPLv3 oleh Omar Roth.",
"Released under the AGPLv3 on Github.": "Dirilis di bawah AGPLv3 di Github.",
"Source available here.": "Sumber tersedia di sini.",
"View JavaScript license information.": "Tampilkan informasi lisensi JavaScript.",
"View privacy policy.": "Lihat kebijakan privasi.",
@ -156,7 +161,11 @@
"Title": "Judul",
"Playlist privacy": "Privasi daftar putar",
"Editing playlist `x`": "Menyunting daftar putar `x`",
"Show more": "Tampilkan lainnya",
"Show less": "Tampilkan lebih sedikit",
"Watch on YouTube": "Tonton di YouTube",
"Switch Invidious Instance": "Beralih Instance Invidious",
"Broken? Try another Invidious Instance": "Rusak? Coba Instance Invidious lain",
"Hide annotations": "Sembunyikan anotasi",
"Show annotations": "Tampilkan anotasi",
"Genre: ": "Genre: ",
@ -169,7 +178,7 @@
"Shared `x`": "Berbagi`x`",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tampilan.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` tampilan."
"": "`x` tampilan"
},
"Premieres in `x`": "Tayang dalam `x`",
"Premieres `x`": "Tayang `x`",
@ -178,7 +187,7 @@
"View more comments on Reddit": "Lihat lebih banyak komentar di Reddit",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Lihat`x` komentar.([^.,0-9]|^)1([^.,0-9]|$)",
"": "Lihat`x` komentar."
"": "Lihat`x` komentar"
},
"View Reddit comments": "Lihat komentar Reddit",
"Hide replies": "Sembunyikan balasan",
@ -205,24 +214,24 @@
"Could not get channel info.": "Tidak bisa mendapatkan info kanal.",
"Could not fetch comments": "Tidak dapat memuat komentar",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Lihat`x` balasan.([^.,0-9]|^)1([^.,0-9]|$)",
"": "Lihat `x` balasan."
"([^.,0-9]|^)1([^.,0-9]|$)": "Lihat`x` balasan",
"": "Lihat `x` balasan"
},
"`x` ago": "`x` lalu",
"Load more": "Muat lebih banyak",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` titik.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` titik."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` titik",
"": "`x` titik"
},
"Could not create mix.": "Tidak dapat membuat mix.",
"Empty playlist": "Daftar putar kosong",
"Not a playlist.": "Bukan daftar putar.",
"Playlist does not exist.": "Daftar putar tidak ada.",
"Could not pull trending pages.": "Tidak bisa mendapatkan laman tren.",
"Hidden field \"challenge\" is a required field": "",
"Hidden field \"token\" is a required field": "",
"Erroneous challenge": "",
"Erroneous token": "",
"Hidden field \"challenge\" is a required field": "Bidang \"tantangan\" tersembunyi wajib diisi",
"Hidden field \"token\" is a required field": "Bidang \"token\" tersembunyi wajib diisi",
"Erroneous challenge": "Tantangan salah",
"Erroneous token": "Token salah",
"No such user": "Tidak ada pengguna demikian",
"Token is expired, please try again": "Token kadaluwarsa, harap coba lagi",
"English": "Bahasa Inggris",
@ -243,7 +252,7 @@
"Cebuano": "Bahasa Cebu",
"Chinese (Simplified)": "Bahasa Cina",
"Chinese (Traditional)": "Bahasa Cina (Tradisonal)",
"Corsican": "",
"Corsican": "Bahasa Korsika",
"Croatian": "Bahasa Kroasia",
"Czech": "Bahasa Ceko",
"Danish": "Bahasa Denmak",
@ -253,17 +262,17 @@
"Filipino": "Bahasa Filipina",
"Finnish": "Bahasa Finlandia",
"French": "Bahasa Perancis",
"Galician": "",
"Galician": "Bahasa Galisia",
"Georgian": "Bahasa Georgia",
"German": "Bahasa Jerman",
"Greek": "Bahasa Yunani",
"Gujarati": "",
"Haitian Creole": "",
"Hausa": "",
"Gujarati": "Bahasa Gujarat",
"Haitian Creole": "Bahasa Kreol Haiti",
"Hausa": "Bahasa Hausa",
"Hawaiian": "Bahasa Hawai",
"Hebrew": "Bahasa Ibrani",
"Hindi": "Bahasa Hindi",
"Hmong": "",
"Hmong": "Bahasa Hmong",
"Hungarian": "Bahasa Hungaria",
"Icelandic": "Bahasa Islandia",
"Igbo": "Bahasa Igbo",
@ -272,53 +281,53 @@
"Italian": "Bahasa Italia",
"Japanese": "Bahasa Jepang",
"Javanese": "Bahasa Jawa",
"Kannada": "",
"Kazakh": "",
"Khmer": "",
"Kannada": "Bahasa Kannada",
"Kazakh": "Bahasa Kazakh",
"Khmer": "Bahasa Khmer",
"Korean": "Bahasa Korea",
"Kurdish": "Bahasa Kurdistan",
"Kyrgyz": "",
"Lao": "",
"Kyrgyz": "Bahasa Kirgiz",
"Lao": "Bahasa Laos",
"Latin": "Bahasa Latin",
"Latvian": "Bahasa Latvia",
"Lithuanian": "Bahasa Lithuania",
"Luxembourgish": "",
"Macedonian": "",
"Malagasy": "",
"Luxembourgish": "Bahasa Luksemburg",
"Macedonian": "Bahasa Makedonia",
"Malagasy": "Bahasa Malagasi",
"Malay": "Bahasa Melayu",
"Malayalam": "",
"Maltese": "",
"Malayalam": "Bahasa Malayalam",
"Maltese": "Bahasa Malta",
"Maori": "Bahasa Maori",
"Marathi": "Bahasa Marathi",
"Mongolian": "Bahasa Mongolia",
"Nepali": "Bahasa Nepal",
"Norwegian Bokmål": "",
"Nyanja": "",
"Pashto": "",
"Norwegian Bokmål": "Bahasa Norwegia Bokmål",
"Nyanja": "Bahasa Chichewa",
"Pashto": "Bahasa Pashtun",
"Persian": "Bahasa Persia",
"Polish": "Bahasa Polandia",
"Portuguese": "Bahasa Portugis",
"Punjabi": "Bahasa Punjabi",
"Romanian": "Bahasa Romania",
"Russian": "Bahasa Russia",
"Samoan": "",
"Scottish Gaelic": "",
"Samoan": "Bahasa Samoa",
"Scottish Gaelic": "Bahasa Gaelik Skotlandia",
"Serbian": "Bahasa Serbia",
"Shona": "",
"Sindhi": "",
"Sinhala": "",
"Shona": "Bahasa Shona",
"Sindhi": "Bahasa Sindhi",
"Sinhala": "Bahasa Sinhala",
"Slovak": "Bahasa Slovakia",
"Slovenian": "Bahasa Slovenia",
"Somali": "Bahasa Somalia",
"Southern Sotho": "",
"Southern Sotho": "Bahasa Sesotho",
"Spanish": "Bahasa Spanyol",
"Spanish (Latin America)": "Bahasa Spanyol (Amerika Latin)",
"Sundanese": "Bahasa Sunda",
"Swahili": "Bahasa Swahili",
"Swedish": "Bahasa Swedia",
"Tajik": "",
"Tajik": "Bahasa Tajik",
"Tamil": "Bahasa Tamil",
"Telugu": "",
"Telugu": "Bahasa Telugu",
"Thai": "Bahasa Thailand",
"Turkish": "Bahasa Turki",
"Ukrainian": "Bahasa Ukraina",
@ -326,56 +335,57 @@
"Uzbek": "Bahasa Uzbek",
"Vietnamese": "Bahasa Vietnam",
"Welsh": "Bahasa Wales",
"Western Frisian": "",
"Xhosa": "",
"Yiddish": "",
"Yoruba": "",
"Zulu": "",
"Western Frisian": "Bahasa Frisia Barat",
"Xhosa": "Bahasa Xhosa",
"Yiddish": "Bahasa Yiddi",
"Yoruba": "Bahasa Yoruba",
"Zulu": "Bahasa Zulu",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tahun.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` tahun."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tahun",
"": "`x` tahun"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` bulan.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` bulan."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` bulan",
"": "`x` bulan"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pekan.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` pekan."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pekan",
"": "`x` pekan"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` hari.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` hari."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` hari",
"": "`x` hari"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` jam.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` jam."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` jam",
"": "`x` jam"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` menit.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` menit."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` menit",
"": "`x` menit"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` detik.([^.,0-9]|^)1([^.,0-9]|$)",
"": "`x` detik."
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` detik",
"": "`x` detik"
},
"Fallback comments: ": "",
"Fallback comments: ": "Komentar mundur: ",
"Popular": "Populer",
"Top": "",
"Search": "Cari",
"Top": "Teratas",
"About": "Ihwal",
"Rating: ": "Peringkat: ",
"Language: ": "Bahasa: ",
"View as playlist": "Tampilkan sebagai daftar putar",
"Default": "Asali",
"Music": "Musik",
"Gaming": "Gaming",
"Gaming": "Permainan",
"News": "Berita",
"Movies": "Film",
"Download": "Unduh",
"Download as: ": "Unduh sebagai: ",
"%A %B %-d, %Y": "",
"%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "(disunting)",
"YouTube comment permalink": "",
"YouTube comment permalink": "Komentar YouTube permalink",
"permalink": "permalink",
"`x` marked it with a ❤": "`x` telah ditandai dengan ❤",
"Audio mode": "Mode audio",
@ -383,5 +393,35 @@
"Videos": "Video",
"Playlists": "Daftar putar",
"Community": "Komunitas",
"Current version: ": "Versi saat ini: "
"relevance": "Relevan",
"rating": "peringkat",
"date": "tanggal",
"views": "ditonton",
"content_type": "tipe_konten",
"duration": "durasi",
"features": "fitur",
"sort": "urut",
"hour": "jam",
"today": "hari ini",
"week": "minggu",
"month": "bulan",
"year": "tahun",
"video": "video",
"channel": "kanal",
"playlist": "daftar putar",
"movie": "film",
"show": "tampilkan",
"hd": "hd",
"subtitles": "subtitel",
"creative_commons": "creative_commons",
"3d": "3d",
"live": "siaran langsung",
"4k": "4k",
"location": "lokasi",
"hdr": "hdr",
"filter": "saring",
"Current version: ": "Versi saat ini: ",
"next_steps_error_message": "Setelah itu Anda harus mencoba: ",
"next_steps_error_message_refresh": "Segarkan",
"next_steps_error_message_go_to_youtube": "Buka YouTube"
}

View File

@ -1,9 +1,16 @@
{
"`x` subscribers": "`x`áskrifendur",
"`x` videos": "`x` myndbönd",
"`x` playlists": "`x` spilunarlistar",
"`x` subscribers.": "`x` áskrifandar.",
"`x` videos.": "`x` myndbönd.",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` áskrifandar",
"": "`x` áskrifendur"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` myndband",
"": "`x` myndbönd"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` spilunarlist",
"": "`x` spilunarlistar"
},
"LIVE": "BEINT",
"Shared `x` ago": "Deilt `x` síðan",
"Unsubscribe": "Afskrá",
@ -64,12 +71,14 @@
"Preferred video quality: ": "Æskilegt myndbands gæði: ",
"Player volume: ": "Spilara hljóðstyrkur: ",
"Default comments: ": "Sjálfgefin ummæli: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Sjálfgefin texti: ",
"Fallback captions: ": "Varatextar: ",
"Show related videos: ": "Sýna tengd myndbönd? ",
"Show annotations by default: ": "Á að sýna glósur sjálfgefið? ",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "Sjónrænar stillingar",
"Player style: ": "Spilara stíl: ",
"Dark mode: ": "Myrkur ham: ",
@ -77,6 +86,8 @@
"dark": "dimmt",
"light": "ljóst",
"Thin mode: ": "Þunnt ham: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Áskriftarstillingar",
"Show annotations by default for subscribed channels: ": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ",
"Redirect homepage to feed: ": "Endurbeina heimasíðu að straumi: ",
@ -106,6 +117,7 @@
"Administrator preferences": "Kjörstillingar stjórnanda",
"Default homepage: ": "Sjálfgefin heimasíða: ",
"Feed menu: ": "Straum valmynd: ",
"Show nickname on top: ": "",
"Top enabled: ": "Toppur virkur? ",
"CAPTCHA enabled: ": "CAPTCHA virk? ",
"Login enabled: ": "Innskráning virk? ",
@ -113,21 +125,27 @@
"Report statistics: ": "Skrá talnagögn? ",
"Save preferences": "Vista stillingar",
"Subscription manager": "Áskriftarstjóri",
"`x` subscriptions": "`x` áskrifendur",
"`x` tokens": "`x` tákn",
"Token manager": "Táknstjóri",
"Token": "Tákn",
"`x` subscriptions.": "`x` áskriftir.",
"`x` tokens.": "`x` tákn.",
"`x` unseen notifications": "`x` óséðar tilkynningar",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` áskriftur",
"": "`x` áskriftir"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tákn",
"": "`x` tákn"
},
"Import/export": "Flytja inn/út",
"unsubscribe": "afskrá",
"revoke": "afturkalla",
"Subscriptions": "Áskriftir",
"`x` unseen notifications.": "`x` óséðar tilkynningar.",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` óséðar tilkynning",
"": "`x` óséðar tilkynningar"
},
"search": "leita",
"Log out": "Útskrá",
"Released under the AGPLv3 by Omar Roth.": "Útgefið undir AGPLv3 eftir Omar Roth.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Frumkóði aðgengilegur hér.",
"View JavaScript license information.": "Skoða JavaScript leyfisupplýsingar.",
"View privacy policy.": "Skoða meðferð persónuupplýsinga.",
@ -143,25 +161,34 @@
"Title": "Titill",
"Playlist privacy": "Spilunarlista opinberri",
"Editing playlist `x`": "Að breyta spilunarlista `x`",
"Show more": "",
"Show less": "",
"Watch on YouTube": "Horfa á YouTube",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "Fela glósur",
"Show annotations": "Sýna glósur",
"Genre: ": "Tegund: ",
"License: ": "Notkunarleyfi: ",
"Family friendly? ": "Fjölskylduvænt? ",
"`x` views": "`x` áhorf",
"Wilson score: ": "Wilson stig: ",
"Engagement: ": "Þátttöku: ",
"Whitelisted regions: ": "Svæði á hvítum lista: ",
"Blacklisted regions: ": "Svæði á svörtum lista: ",
"Shared `x`": "Deilt `x`",
"`x` views.": "`x` áhorf.",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` áhorf",
"": "`x` áhorf"
},
"Premieres in `x`": "Frumflutt eftir `x`",
"Premieres `x`": "Frumflutt `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.": "Hæ! Lítur út eins og þú hafir slökkt á JavaScript. Smelltu hér til að skoða ummæli, hafðu í huga að þær geta tekið aðeins lengri tíma að hlaða.",
"View YouTube comments": "Skoða YouTube ummæli",
"View more comments on Reddit": "Skoða fleiri ummæli á Reddit",
"View `x` comments": "Skoða `x` ummæli",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Skoða `x` ummæli",
"": "Skoða `x` ummæli"
},
"View Reddit comments": "Skoða Reddit ummæli",
"Hide replies": "Fela svör",
"Show replies": "Sýna svör",
@ -180,18 +207,22 @@
"Password cannot be empty": "Lykilorð má ekki vera autt",
"Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir",
"Please log in": "Vinsamlegast skráðu þig inn",
"View `x` replies": "Skoða `x` svör",
"Invidious Private Feed for `x`": "Invidious Persónulegur Straumur fyrir `x`",
"channel:`x`": "rás:`x`",
"`x` points": "`x` stig",
"Deleted or invalid channel": "Eytt eða ógild rás",
"This channel does not exist.": "Þessi rás er ekki til.",
"Could not get channel info.": "Ekki tókst að fá rásarupplýsingar.",
"Could not fetch comments": "Ekki tókst að sækja ummæli",
"View `x` replies.": "Skoða `x` svör.",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Skoða `x` svar",
"": "Skoða `x` svör"
},
"`x` ago": "`x` síðan",
"Load more": "Hlaða meira",
"`x` points.": "`x` stig.",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` stig",
"": "`x` stig"
},
"Could not create mix.": "Ekki tókst að búa til blöndu.",
"Empty playlist": "Tómur spilunarlisti",
"Not a playlist.": "Ekki spilunarlisti.",
@ -301,13 +332,6 @@
"Turkish": "Tyrkneska",
"Ukrainian": "Úkraníska",
"Urdu": "Úrdú",
"`x` years": "`x` ár",
"`x` months": "`x` mánuði",
"`x` weeks": "`x` vikur",
"`x` days": "`x` daga",
"`x` hours": "`x` klukkustundir",
"`x` minutes": "`x` mínútur",
"`x` seconds": "`x` sekúndur",
"Uzbek": "Úsbekíska",
"Vietnamese": "Víetnamska",
"Welsh": "Velska",
@ -316,22 +340,42 @@
"Yiddish": "Jiddíska",
"Yoruba": "Jórúba",
"Zulu": "Zúlú",
"`x` years.": "`x` ár.",
"`x` months.": "`x` mánuði.",
"`x` weeks.": "`x` vikur.",
"`x` days.": "`x` dagar.",
"`x` hours.": "`x` klukkustundir.",
"`x` minutes.": "`x` mínútur.",
"`x` seconds.": "`x` sekúndur.",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ár",
"": "`x` ár"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` mánuð",
"": "`x` mánuði"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` vika",
"": "`x` vikur"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dagur",
"": "`x` dagar"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` klukkustund",
"": "`x` klukkustundir"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` mínúta",
"": "`x` mínútur"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` sekúnda",
"": "`x` sekúndur"
},
"Fallback comments: ": "Vara ummæli: ",
"Popular": "Vinsælt",
"permalink": "Varanlegur tengill",
"Search": "",
"Top": "Topp",
"About": "Um",
"Rating: ": "Einkunn: ",
"Language: ": "Tungumál: ",
"View as playlist": "Skoða sem spilunarlista",
"Community": "Samfélag",
"Default": "Sjálfgefið",
"Music": "Tónlist",
"Gaming": "Tólvuleikja",
@ -342,10 +386,42 @@
"%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "(breytt)",
"YouTube comment permalink": "YouTube ummæli varanlegur tengill",
"permalink": "Varanlegur tengill",
"`x` marked it with a ❤": "`x` merkti það með ❤",
"Audio mode": "Hljóð ham",
"Video mode": "Myndband ham",
"Videos": "Myndbönd",
"Playlists": "Spilunarlistar",
"Current version: ": "Núverandi útgáfa: "
"Community": "Samfélag",
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "Núverandi útgáfa: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,9 +1,16 @@
{
"`x` subscribers..([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscritto",
"`x` subscribers..": "`x` iscritti.",
"`x` videos..([^.,0-9]|^)1([^.,0-9]|$)": "`x` video",
"`x` videos..": "`x` video.",
"`x` playlists": "`x` playlist",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscritto",
"": "`x` iscritti"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` video",
"": "`x` video"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` playlist",
"": "`x` playlist"
},
"LIVE": "IN DIRETTA",
"Shared `x` ago": "Condiviso `x` fa",
"Unsubscribe": "Disiscriviti",
@ -70,6 +77,8 @@
"Fallback captions: ": "Sottotitoli alternativi: ",
"Show related videos: ": "Mostra video correlati: ",
"Show annotations by default: ": "Mostra le annotazioni in modo predefinito: ",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "Preferenze grafiche",
"Player style: ": "Stile riproduttore: ",
"Dark mode: ": "Tema scuro: ",
@ -77,6 +86,8 @@
"dark": "scuro",
"light": "chiaro",
"Thin mode: ": "Modalità per connessioni lente: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Preferenze iscrizioni",
"Show annotations by default for subscribed channels: ": "Mostrare annotazioni in modo predefinito per i canali sottoscritti: ",
"Redirect homepage to feed: ": "Reindirizza la pagina principale a quella delle iscrizioni: ",
@ -106,6 +117,7 @@
"Administrator preferences": "Preferenze amministratore",
"Default homepage: ": "Pagina principale predefinita: ",
"Feed menu: ": "Menu iscrizioni: ",
"Show nickname on top: ": "",
"Top enabled: ": "Top abilitato: ",
"CAPTCHA enabled: ": "CAPTCHA attivati: ",
"Login enabled: ": "Accesso attivato: ",
@ -115,19 +127,25 @@
"Subscription manager": "Gestione delle iscrizioni",
"Token manager": "Gestione dei gettoni",
"Token": "Gettone",
"`x` subscriptions..([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscrizione",
"`x` subscriptions..": "`x` iscrizioni.",
"`x` tokens..([^.,0-9]|^)1([^.,0-9]|$)": "`x` gettone",
"`x` tokens..": "`x` gettoni.",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscrizione",
"": "`x` iscrizioni"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` gettone",
"": "`x` gettoni"
},
"Import/export": "Importa/esporta",
"unsubscribe": "disiscriviti",
"revoke": "revoca",
"Subscriptions": "Iscrizioni",
"`x` unseen notifications..([^.,0-9]|^)1([^.,0-9]|$)": "`x` notifica non visualizzata",
"`x` unseen notifications..": "`x` notifiche non visualizzate.",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` notifica non visualizzata",
"": "`x` notifiche non visualizzate"
},
"search": "Cerca",
"Log out": "Esci",
"Released under the AGPLv3 by Omar Roth.": "Pubblicato con licenza AGPLv3 da Omar Roth.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Codice sorgente.",
"View JavaScript license information.": "Guarda le informazioni di licenza del codice JavaScript.",
"View privacy policy.": "Vedi la politica sulla privacy.",
@ -143,7 +161,11 @@
"Title": "Titolo",
"Playlist privacy": "Privacy playlist",
"Editing playlist `x`": "Modificando la playlist `x`",
"Show more": "",
"Show less": "",
"Watch on YouTube": "Guarda su YouTube",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "Nascondi annotazioni",
"Show annotations": "Mostra annotazioni",
"Genre: ": "Genere: ",
@ -154,14 +176,19 @@
"Whitelisted regions: ": "Regioni in lista bianca: ",
"Blacklisted regions: ": "Regioni in lista nera: ",
"Shared `x`": "Condiviso `x`",
"`x` views..([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizzazione",
"`x` views..": "`x` visualizzazioni.",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizzazione",
"": "`x` visualizzazioni"
},
"Premieres in `x`": "In anteprima in `x`",
"Premieres `x`": "In anteprima `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.": "Ciao! Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti. Considera che potrebbe volerci più tempo.",
"View YouTube comments": "Visualizza i commenti da YouTube",
"View more comments on Reddit": "Visualizza più commenti su Reddit",
"View `x` comments": "Visualizza `x` commenti",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Visualizza `x` commento",
"": "Visualizza `x` commenti"
},
"View Reddit comments": "Visualizza i commenti da Reddit",
"Hide replies": "Nascondi le risposte",
"Show replies": "Mostra le risposte",
@ -186,12 +213,16 @@
"This channel does not exist.": "Questo canale non esiste.",
"Could not get channel info.": "Impossibile ottenere le informazioni del canale.",
"Could not fetch comments": "Impossibile recuperare i commenti",
"View `x` replies..([^.,0-9]|^)1([^.,0-9]|$)": "Visualizza `x` risposta",
"View `x` replies..": "Visualizza `x` risposte.",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Visualizza `x` risposta",
"": "Visualizza `x` risposte"
},
"`x` ago": "`x` fa",
"Load more": "Carica altro",
"`x` points..([^.,0-9]|^)1([^.,0-9]|$)": "`x` punto",
"`x` points..": "`x` punti.",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` punto",
"": "`x` punti"
},
"Could not create mix.": "Impossibile creare il mix.",
"Empty playlist": "Playlist vuota",
"Not a playlist.": "Non è una playlist.",
@ -309,22 +340,37 @@
"Yiddish": "Yiddish",
"Yoruba": "Yoruba",
"Zulu": "Zulu",
"`x` years..([^.,0-9]|^)1([^.,0-9]|$)": "`x` anno",
"`x` years..": "`x` anni.",
"`x` months..([^.,0-9]|^)1([^.,0-9]|$)": "`x` mese",
"`x` months..": "`x` mesi.",
"`x` weeks..([^.,0-9]|^)1([^.,0-9]|$)": "`x` settimana",
"`x` weeks..": "`x` settimane.",
"`x` days..([^.,0-9]|^)1([^.,0-9]|$)": "`x` giorno",
"`x` days..": "`x` giorni.",
"`x` hours..([^.,0-9]|^)1([^.,0-9]|$)": "`x` ora",
"`x` hours..": "`x` ore.",
"`x` minutes..([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto",
"`x` minutes..": "`x` minuti.",
"`x` seconds..([^.,0-9]|^)1([^.,0-9]|$)": "`x` secondo",
"`x` seconds..": "`x` secondi.",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` anno",
"": "`x` anni"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` mese",
"": "`x` mesi"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` settimana",
"": "`x` settimane"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` giorno",
"": "`x` giorni"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ora",
"": "`x` ore"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto",
"": "`x` minuti"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` secondo",
"": "`x` secondi"
},
"Fallback comments: ": "Commenti alternativi: ",
"Popular": "Popolare",
"Search": "Cerca",
"Top": "Top",
"About": "Al riguardo",
"Rating: ": "Punteggio: ",
@ -347,5 +393,35 @@
"Videos": "Video",
"Playlists": "Playlist",
"Community": "Comunità",
"Current version: ": "Versione attuale: "
"relevance": "Pertinenza",
"rating": "Valutazione",
"date": "Data di caricamento",
"views": "Numero di visualizzazioni",
"content_type": "Tipo",
"duration": "Durata",
"features": "Caratteristiche",
"sort": "Ordina per",
"hour": "Ultima ora",
"today": "Oggi",
"week": "Questa settimana",
"month": "Questo mese",
"year": "Quest'anno",
"video": "Video",
"channel": "Canale",
"playlist": "Playlist",
"movie": "Film",
"show": "",
"hd": "AD",
"subtitles": "Sottotitoli / CC",
"creative_commons": "Creative Commons",
"3d": "3D",
"live": "In diretta",
"4k": "4K",
"location": "Posizione",
"hdr": "HDR",
"filter": "Filtra",
"Current version: ": "Versione attuale: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -71,12 +71,14 @@
"Preferred video quality: ": "優先する画質: ",
"Player volume: ": "プレイヤーの音量: ",
"Default comments: ": "デフォルトのコメント: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "デフォルトの字幕: ",
"Fallback captions: ": "フォールバック時の字幕: ",
"Show related videos: ": "関連動画を表示: ",
"Show annotations by default: ": "デフォルトでアノテーションを表示: ",
"Automatically extend video description: ": "動画の説明文を自動的に拡張: ",
"Interactive 360 degree videos: ": "インタラクティブ360°動画: ",
"Visual preferences": "外観設定",
"Player style: ": "プレイヤースタイル: ",
"Dark mode: ": "ダークモード: ",
@ -84,6 +86,8 @@
"dark": "ダーク",
"light": "ライト",
"Thin mode: ": "最小モード: ",
"Miscellaneous preferences": "雑設定",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "自動インスタンスの移転redirect.invidious.ioにフォールバック ",
"Subscription preferences": "登録チャンネル設定",
"Show annotations by default for subscribed channels: ": "デフォルトで登録チャンネルのアノテーションを表示しますか? ",
"Redirect homepage to feed: ": "ホームからフィードにリダイレクト: ",
@ -113,6 +117,7 @@
"Administrator preferences": "管理者設定",
"Default homepage: ": "デフォルトのホーム: ",
"Feed menu: ": "フィードメニュー: ",
"Show nickname on top: ": "ニックネームを一番上に表示する: ",
"Top enabled: ": "トップページを有効化: ",
"CAPTCHA enabled: ": "CAPTCHA を有効化: ",
"Login enabled: ": "ログインを有効化: ",
@ -140,7 +145,7 @@
},
"search": "検索",
"Log out": "ログアウト",
"Released under the AGPLv3 by Omar Roth.": "Omar Roth によって AGPLv3 でリリースされています",
"Released under the AGPLv3 on Github.": "Github 上で AGPLv3 の下で公開されています",
"Source available here.": "ソースはここで閲覧可能です。",
"View JavaScript license information.": "JavaScript ライセンス情報",
"View privacy policy.": "プライバシーポリシー",
@ -156,7 +161,11 @@
"Title": "タイトル",
"Playlist privacy": "再生リストのプライバシー",
"Editing playlist `x`": "再生リスト `x` を編集中",
"Show more": "表示を増やす",
"Show less": "表示を減らす",
"Watch on YouTube": "YouTube で視聴",
"Switch Invidious Instance": "Invidiousインスタンスの変更",
"Broken? Try another Invidious Instance": "壊れる違うInvidiousインスタンスを試してみる",
"Hide annotations": "アノテーションを隠す",
"Show annotations": "アノテーションを表示",
"Genre: ": "ジャンル: ",
@ -361,6 +370,7 @@
},
"Fallback comments: ": "フォールバック時のコメント: ",
"Popular": "人気",
"Search": "検索",
"Top": "トップ",
"About": "このサービスについて",
"Rating: ": "評価: ",
@ -383,5 +393,35 @@
"Videos": "動画",
"Playlists": "プレイリスト",
"Community": "コミュニティ",
"Current version: ": "現在のバージョン: "
"relevance": "関連",
"rating": "評価",
"date": "時刻",
"views": "再生回数",
"content_type": "コンテンツの種類",
"duration": "再生時間",
"features": "機能",
"sort": "順番",
"hour": "1時間前",
"today": "今日",
"week": "今週",
"month": "今月",
"year": "今年",
"video": "動画",
"channel": "チャンネル",
"playlist": "再生リスト",
"movie": "映画",
"show": "番組",
"hd": "HD",
"subtitles": "字幕",
"creative_commons": "クリエイティブ・コモンズ",
"3d": "3D",
"live": "生配信",
"4k": "4K",
"location": "場所",
"hdr": "HDR",
"filter": "フィルタ",
"Current version: ": "現在のバージョン: ",
"next_steps_error_message": "下記のものを試して下さい: ",
"next_steps_error_message_refresh": "再読込",
"next_steps_error_message_go_to_youtube": "YouTubeへ"
}

427
locales/ko.json Normal file
View File

@ -0,0 +1,427 @@
{
"Sort videos by: ": "동영상 정렬 기준: ",
"Number of videos shown in feed: ": "피드에 표시된 동영상 수: ",
"Redirect homepage to feed: ": "피드로 홈페이지 리디렉션: ",
"Show annotations by default for subscribed channels: ": "구독한 채널에 기본적으로 특수효과를 표시하시겠습니까? ",
"Subscription preferences": "구독 설정",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "자동 인스턴스 리디렉션 (redirect.invidious.io로 대체): ",
"Thin mode: ": "단순 모드: ",
"light": "라이트",
"dark": "다크",
"Theme: ": "테마: ",
"Dark mode: ": "다크 모드: ",
"Player style: ": "플레이어 스타일: ",
"Visual preferences": "시각 설정",
"Interactive 360 degree videos: ": "인터랙티브 360도 비디오: ",
"Automatically extend video description: ": "자동으로 비디오 설명 확장: ",
"Show annotations by default: ": "기본적으로 주석 표시: ",
"Show related videos: ": "관련 동영상 보기: ",
"Fallback captions: ": "대체 자막: ",
"Default captions: ": "기본 자막: ",
"reddit": "Reddit",
"youtube": "YouTube",
"Default comments: ": "기본 댓글: ",
"Player volume: ": "플레이어 볼륨: ",
"Preferred video quality: ": "선호하는 비디오 품질: ",
"Default speed: ": "기본 속도: ",
"Proxy videos: ": "비디오를 프록시: ",
"Listen by default: ": "기본적으로 듣기: ",
"Autoplay next video: ": "다음 동영상 자동재생 ",
"Play next by default: ": "기본적으로 다음 재생: ",
"Autoplay: ": "자동재생: ",
"Always loop: ": "항상 반복: ",
"Player preferences": "플레이어 설정",
"Preferences": "설정",
"Google verification code": "구글 인증 코드",
"E-mail": "이메일",
"Register": "회원가입",
"Sign In": "로그인",
"Miscellaneous preferences": "기타 설정",
"Image CAPTCHA": "이미지 CAPTCHA",
"Text CAPTCHA": "텍스트 CAPTCHA",
"Time (h:mm:ss):": "시각 (h:mm:ss):",
"Password": "비밀번호",
"User ID": "사용자 ID",
"Log in with Google": "구글로 로그인",
"Log in/register": "로그인/회원가입",
"Log in": "로그인",
"source": "출처",
"JavaScript license information": "JavaScript 라이선스 정보",
"An alternative front-end to YouTube": "YouTube의 대안 프론트엔드",
"History": "역사",
"Delete account?": "계정을 삭제 하시겠습니까?",
"Export data as JSON": "데이터를 JSON으로 내보내기",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "구독을 OPML로 내보내기 (NewPipe 및 FreeTube 용)",
"Export subscriptions as OPML": "구독을 OPML로 내보내기",
"Export": "내보내기",
"Import NewPipe data (.zip)": "NewPipe 데이터 가져오기 (.zip)",
"Import NewPipe subscriptions (.json)": "NewPipe 구독을 가져오기 (.json)",
"Import FreeTube subscriptions (.db)": "FreeTube 구독 가져오기 (.db)",
"Import YouTube subscriptions": "YouTube 구독 가져오기",
"Import Invidious data": "Invidious 데이터 가져오기",
"Import": "가져오기",
"Import and Export Data": "데이터 가져오기 및 내보내기",
"No": "아니요",
"Yes": "예",
"Authorize token for `x`?": "`x` 에 대한 토큰을 승인하시겠습니까?",
"Authorize token?": "토큰을 승인하시겠습니까?",
"Cannot change password for Google accounts": "Google 계정의 비밀번호를 변경할 수 없습니다",
"New passwords must match": "새 비밀번호는 일치해야 합니다",
"New password": "새 비밀번호",
"Clear watch history?": "재생 기록을 삭제 하시겠습니까?",
"Previous page": "이전 페이지",
"Next page": "다음 페이지",
"last": "마지막",
"Shared `x` ago": "`x` 전에 공유",
"popular": "인기",
"oldest": "오래된순",
"newest": "최신순",
"View playlist on YouTube": "YouTube에서 재생목록 보기",
"View channel on YouTube": "YouTube에서 채널 보기",
"Subscribe": "구독",
"Unsubscribe": "구독 취소",
"LIVE": "실시간",
"`x` playlists": {
"": "`x` 재생목록",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 재생목록"
},
"`x` videos": {
"": "`x` 동영상",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 동영상"
},
"`x` subscribers": {
"": "`x` 구독자",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 구독자"
},
"playlist": "재생목록",
"Korean": "한국어",
"Japanese": "일본어",
"Greek": "그리스어",
"German": "독일어",
"Chinese (Traditional)": "중국어 (정자)",
"Chinese (Simplified)": "중국어 (간체자)",
"French": "프랑스어",
"Finnish": "핀란드어",
"Basque": "바스크어",
"Bangla": "벵골어",
"Azerbaijani": "아제르바이잔어",
"Armenian": "아르메니아어",
"Arabic": "아랍어",
"Amharic": "암하라어",
"Albanian": "알바니아어",
"Afrikaans": "아프리카어",
"English (auto-generated)": "영어 (자동 생성됨)",
"English": "영어",
"Token is expired, please try again": "토큰이 만료되었습니다. 다시 시도해 주세요",
"Load more": "더 불러오기",
"Could not fetch comments": "댓글을 가져올 수 없습니다",
"Could not get channel info.": "채널 정보를 가져올 수 없습니다.",
"This channel does not exist.": "이 채널은 존재하지 않습니다.",
"Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널",
"channel:`x`": "채널:`x`",
"Invalid TFA code": "유효하지 않은 TFA 코드",
"Show replies": "댓글 보기",
"Hide replies": "댓글 숨기기",
"Incorrect password": "잘못된 비밀번호",
"License: ": "라이선스: ",
"Genre: ": "장르: ",
"Editing playlist `x`": "재생목록 `x` 수정하기",
"Playlist privacy": "재생목록 공개 범위",
"Watch on YouTube": "YouTube에서 보기",
"Show less": "간략히",
"Show more": "더보기",
"Title": "제목",
"Create playlist": "재생목록 생성",
"Trending": "급상승",
"Delete playlist": "재생목록 삭제",
"Delete playlist `x`?": "재생목록 `x` 를 삭제 하시겠습니까?",
"Updated `x` ago": "`x` 전에 업데이트됨",
"Released under the AGPLv3 on Github.": "Github에 AGPLv3 으로 배포됩니다.",
"View all playlists": "모든 재생목록 보기",
"Private": "비공개",
"Unlisted": "목록에 없음",
"Public": "공개",
"View privacy policy.": "개인정보 처리방침 보기.",
"View JavaScript license information.": "JavaScript 라이센스 정보 보기.",
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
"Log out": "로그아웃",
"search": "검색",
"`x` unseen notifications": {
"": "`x` 읽지 않은 알림",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 읽지 않은 알림"
},
"Subscriptions": "구독",
"revoke": "철회",
"unsubscribe": "구독 취소",
"Import/export": "가져오기/내보내기",
"`x` tokens": {
"": "`x` 토큰",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 토큰"
},
"`x` subscriptions": {
"": "`x` 구독",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 구독"
},
"Token": "토큰",
"Token manager": "토큰 관리자",
"Subscription manager": "구독 관리자",
"Save preferences": "설정 저장",
"Report statistics: ": "통계 보고: ",
"Registration enabled: ": "등록 활성화: ",
"Login enabled: ": "로그인 활성화: ",
"CAPTCHA enabled: ": "CAPTCHA 활성화: ",
"Top enabled: ": "Top 활성화: ",
"Show nickname on top: ": "상단에 닉네임 표시: ",
"Feed menu: ": "피드 메뉴: ",
"Default homepage: ": "기본 홈페이지: ",
"Administrator preferences": "관리자 설정",
"Delete account": "계정 삭제",
"Watch history": "시청 기록",
"Manage tokens": "토큰 관리",
"Manage subscriptions": "구독 관리",
"Change password": "비밀번호 변경",
"Import/export data": "데이터 가져오기/내보내기",
"Clear watch history": "시청 기록 지우기",
"Data preferences": "데이터 설정",
"`x` is live": "`x` 이(가) 라이브 중입니다",
"`x` uploaded a video": "`x` 동영상 게시됨",
"Enable web notifications": "웹 알림 활성화",
"Only show notifications (if there are any): ": "알림만 표시 (있는 경우): ",
"Only show unwatched: ": "시청하지 않은 것만 표시: ",
"Only show latest unwatched video from channel: ": "채널의 시청하지 않은 최신 동영상만 표시: ",
"Only show latest video from channel: ": "채널의 최신 동영상만 표시: ",
"channel name - reverse": "채널 이름 - 역순",
"alphabetically - reverse": "알파벳순 - 역순",
"published - reverse": "게시일 - 역순",
"published": "게시일",
"channel name": "채널 이름",
"alphabetically": "알파벳순",
"Samoan": "사모아어",
"Russian": "러시아어",
"Romanian": "루마니아어",
"Punjabi": "펀자브어",
"Portuguese": "포르투갈어(포어)",
"Polish": "폴란드어",
"Persian": "페르시아어(파사어)",
"Pashto": "파슈토어",
"Nyanja": "체와어",
"Norwegian Bokmål": "보크몰",
"Nepali": "네팔어",
"Mongolian": "몽골어",
"Marathi": "마라티어",
"Maori": "마오리어",
"Maltese": "몰타어",
"Wrong answer": "잘못된 답변",
"live": "실시간",
"3d": "3D",
"location": "지역",
"4k": "4K",
"filter": "필터",
"hdr": "HDR",
"Current version: ": "현재 버전: ",
"next_steps_error_message_refresh": "새로 고침",
"next_steps_error_message_go_to_youtube": "YouTube로 가기",
"subtitles": "자막",
"`x` marked it with a ❤": "`x`님의 ❤",
"Download as: ": "다음으로 다운로드: ",
"Download": "다운로드",
"Search": "검색",
"Language: ": "언어: ",
"Malayalam": "말라얄람어",
"Malay": "말레이어",
"Malagasy": "말라가시어",
"Macedonian": "마케도니아어",
"Luxembourgish": "룩셈부르크어",
"Lithuanian": "리투아니아어",
"Latvian": "라트비아어",
"Latin": "라틴어",
"Lao": "라오어",
"channel": "채널",
"Kyrgyz": "키르기스어",
"Kurdish": "쿠르드어",
"Khmer": "크메르어",
"Kazakh": "카자흐어",
"Kannada": "칸나다어",
"Javanese": "자바어",
"Italian": "이탈리아어(이태리어)",
"Irish": "아일랜드어",
"Indonesian": "인도네시아어",
"Igbo": "이보어",
"Icelandic": "아이슬란드어",
"Hungarian": "헝가리어",
"Hmong": "몽어",
"Hindi": "힌디어",
"Hebrew": "히브리어",
"Hawaiian": "하와이어",
"Hausa": "하우사어",
"No such user": "해당 사용자 없음",
"Erroneous token": "잘못된 token",
"Erroneous challenge": "잘못된 challenge",
"Hidden field \"token\" is a required field": "숨겨진 필드 \"token\"은 필수 필드입니다",
"Hidden field \"challenge\" is a required field": "숨겨진 필드 \"challenge\"는 필수 필드입니다",
"Could not pull trending pages.": "인기 급상승 페이지를 가져올 수 없습니다.",
"Could not create mix.": "믹스를 생성할 수 없습니다.",
"`x` ago": "`x` 전",
"View `x` replies": {
"": "답글 `x`개 보기",
"([^.,0-9]|^)1([^.,0-9]|$)": "답글 `x`개 보기"
},
"View Reddit comments": "Reddit의 댓글 보기",
"Engagement: ": "약속: ",
"Wilson score: ": "Wilson Score: ",
"Family friendly? ": "가족 친화적입니까? ",
"Quota exceeded, try again in a few hours": "한도량을 초과했습니다. 몇 시간 후에 다시 시도하세요",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 개의 댓글 보기",
"": "`x` 개의 댓글 보기"
},
"Haitian Creole": "아이티 크레올어",
"Gujarati": "구자라트어",
"Esperanto": "에스페란토(에스페란토어)",
"Georgian": "조지아어",
"Galician": "갈리시아어",
"Filipino": "타갈로그어(필리핀어)",
"Estonian": "에스토니아어",
"Dutch": "네덜란드어",
"Danish": "덴마크어",
"Czech": "체코어",
"Croatian": "크로아티아어",
"Corsican": "코르시카어",
"Cebuano": "세부아노어",
"Catalan": "카탈루냐어",
"Burmese": "버마어",
"Bulgarian": "불가리아어",
"Bosnian": "보스니아어",
"Belarusian": "벨라루스어",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "로그인할 수 없습니다. 이중 인증(Authenticator 또는 SMS)이 켜져 있는지 확인하세요.",
"View more comments on Reddit": "Reddit에서 더 많은 댓글 보기",
"View YouTube comments": "YouTube 댓글 보기",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "JavaScript가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
"Shared `x`": "공유된 `x`",
"Whitelisted regions: ": "차단되지 않은 지역: ",
"views": "조회수",
"`x` views": {
"": "`x` 조회수",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 조회수"
},
"Please log in": "로그인하세요",
"Password cannot be longer than 55 characters": "비밀번호는 55자 이하여야 합니다",
"Password cannot be empty": "비밀번호는 비워둘 수 없습니다",
"Please sign in using 'Log in with Google'": "'Google로 로그인'을 사용하여 로그인하세요",
"Wrong username or password": "잘못된 사용자 이름 또는 비밀번호",
"Password is a required field": "비밀번호는 필수 필드입니다",
"User ID is a required field": "사용자 ID는 필수 필드입니다",
"CAPTCHA is a required field": "CAPTCHA는 필수 필드입니다",
"Erroneous CAPTCHA": "잘못된 CAPTCHA",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "로그인 실패. 계정에 이중 인증이 설정되어 있지 않기 때문일 수 있습니다.",
"Blacklisted regions: ": "차단된 지역: ",
"Playlists": "재생목록",
"View as playlist": "재생목록으로 보기",
"Playlist does not exist.": "재생목록이 존재하지 않음.",
"Not a playlist.": "재생목록이 아님.",
"Empty playlist": "재생목록 비어 있음",
"Show annotations": "주석 보이기",
"Hide annotations": "주석 숨기기",
"Broken? Try another Invidious Instance": "안되나요? 다른 Invidious 인스턴스를 시도해보세요",
"Switch Invidious Instance": "Invidious 인스턴스 변경",
"Spanish": "스페인어",
"Southern Sotho": "소토어",
"Somali": "소말리어",
"Slovenian": "슬로베니아어",
"Slovak": "슬로바키아어",
"Sinhala": "싱할라어",
"Sindhi": "신드어",
"Shona": "쇼나어",
"Serbian": "세르비아어",
"Scottish Gaelic": "스코틀랜드 게일어",
"Popular": "인기",
"Fallback comments: ": "대체 댓글: ",
"`x` seconds": {
"": "`x` 초",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 초"
},
"Swahili": "스와힐리어",
"Sundanese": "순다어",
"`x` hours": {
"": "`x` 시",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 시"
},
"`x` minutes": {
"": "`x` 분",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 분"
},
"`x` days": {
"": "`x` 일",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 일"
},
"`x` weeks": {
"": "`x` 주",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 주"
},
"`x` months": {
"": "`x` 월",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 월"
},
"`x` years": {
"": "`x` 년",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 년"
},
"Zulu": "줄루어",
"Yoruba": "요루바어",
"Yiddish": "이디시어",
"Xhosa": "코사어",
"Western Frisian": "서부 프리지아어",
"Welsh": "웨일스어",
"Vietnamese": "베트남어",
"Uzbek": "우즈베크어",
"Urdu": "우르두어",
"Ukrainian": "우크라이나어",
"Turkish": "터키어",
"Thai": "태국어",
"Telugu": "텔루구어",
"Tamil": "타밀어",
"Tajik": "타지크어",
"Swedish": "스웨덴어",
"Spanish (Latin America)": "스페인어 (라틴 아메리카)",
"`x` points": {
"": "`x` 포인트",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 포인트"
},
"Invidious Private Feed for `x`": "`x` 에 대한 Invidious 비공개 피드",
"Premieres `x`": "최초 공개 `x`",
"Premieres in `x`": "`x` 에 최초 공개",
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
"creative_commons": "크리에이티브 커먼즈",
"duration": "길이",
"content_type": "구분",
"date": "업로드 날짜",
"rating": "평점",
"relevance": "관련성",
"Community": "커뮤니티",
"Videos": "동영상",
"Video mode": "비디오 모드",
"Audio mode": "오디오 모드",
"permalink": "퍼머링크",
"YouTube comment permalink": "YouTube 댓글 퍼머링크",
"(edited)": "(수정됨)",
"%A %B %-d, %Y": "%A %B %-d, %Y",
"Movies": "영화",
"News": "뉴스",
"Gaming": "게임",
"Music": "음악",
"Default": "디폴트",
"Rating: ": "평점: ",
"About": "정보",
"Top": "최고",
"hd": "HD",
"show": "쇼",
"movie": "영화",
"video": "동영상",
"year": "올해",
"month": "이번 달",
"week": "이번 주",
"today": "오늘",
"hour": "지난 1시간",
"sort": "정렬기준",
"features": "기능별"
}

427
locales/lt.json Normal file
View File

@ -0,0 +1,427 @@
{
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` prenumeratorius",
"": "`x` prenumeratoriai"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` vaizdo įrašas",
"": "`x` vaizdo įrašai"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` grojaraštis",
"": "`x` grojaraščiai"
},
"LIVE": "LIVE",
"Shared `x` ago": "Pasidalino prieš `x`",
"Unsubscribe": "Atšaukti prenumeratą",
"Subscribe": "Prenumeruoti",
"View channel on YouTube": "Peržiūrėti kanalą YouTube",
"View playlist on YouTube": "Peržiūrėti grojaraštį YouTube",
"newest": "naujausia",
"oldest": "seniausia",
"popular": "populiaru",
"last": "paskutinis",
"Next page": "Kitas puslapis",
"Previous page": "Ankstesnis puslapis",
"Clear watch history?": "Išvalyti žiūrėjimo istoriją?",
"New password": "Naujas slaptažodis",
"New passwords must match": "Naujas slaptažodis turi sutapti",
"Cannot change password for Google accounts": "Negalima pakeisti Google paskyros slaptažodžio",
"Authorize token?": "Autorizuoti žetoną?",
"Authorize token for `x`?": "Autorizuoti žetoną `x`?",
"Yes": "Taip",
"No": "Ne",
"Import and Export Data": "Importuoti ir eksportuoti duomenis",
"Import": "Importuoti",
"Import Invidious data": "Importuoti Invidious duomenis",
"Import YouTube subscriptions": "Importuoti YouTube prenumeratas",
"Import FreeTube subscriptions (.db)": "Importuoti FreeTube prenumeratas (.db)",
"Import NewPipe subscriptions (.json)": "Importuoti NewPipe prenumeratas (.json)",
"Import NewPipe data (.zip)": "Importuoti NewPipe duomenis (.zip)",
"Export": "Eksportuoti",
"Export subscriptions as OPML": "Eksportuoti prenumeratas kaip OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuoti prenumeratas kaip OPML (skirta NewPipe & FreeTube)",
"Export data as JSON": "Eksportuoti duomenis kaip JSON",
"Delete account?": "Ištrinti paskyrą?",
"History": "Istorija",
"An alternative front-end to YouTube": "Alternatyvus YouTube žiūrėjimo būdas",
"JavaScript license information": "JavaScript licencijos informacija",
"source": "šaltinis",
"Log in": "Prisijungti",
"Log in/register": "Prisijungti/ registruotis",
"Log in with Google": "Prisijungti naudojantis Google",
"User ID": "Naudotojo ID",
"Password": "Slaptažodis",
"Time (h:mm:ss):": "Laikas (h:mm:ss):",
"Text CAPTCHA": "CAPTCHA tekstas",
"Image CAPTCHA": "CAPTCHA paveikslėlis",
"Sign In": "Prisijungti",
"Register": "Registruotis",
"E-mail": "El. paštas",
"Google verification code": "Google patvirtinimo kodas",
"Preferences": "Pasirinktys",
"Player preferences": "Grotuvo pasirinktys",
"Always loop: ": "Visada kartoti: ",
"Autoplay: ": "Leisti automatiškai: ",
"Play next by default: ": "Leisti sekantį automatiškai kaip nustatyta: ",
"Autoplay next video: ": "Automatiškai leisti sekantį vaizdo įrašą: ",
"Listen by default: ": "Klausytis kaip nustatyta: ",
"Proxy videos: ": "Vaizdo įrašams naudoti proxy: ",
"Default speed: ": "Numatytasis greitis: ",
"Preferred video quality: ": "Pageidaujama vaizdo kokybė: ",
"Player volume: ": "Grotuvo garsas: ",
"Default comments: ": "Numatytieji komentarai: ",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Numatytieji subtitrai: ",
"Fallback captions: ": "Atsarginiai subtitrai: ",
"Show related videos: ": "Rodyti susijusius vaizdo įrašus: ",
"Show annotations by default: ": "Rodyti anotacijas pagal nutylėjimą: ",
"Automatically extend video description: ": "Automatiškai išplėsti vaizdo įrašo aprašymą: ",
"Interactive 360 degree videos: ": "Interaktyvūs 360 laipsnių vaizdo įrašai: ",
"Visual preferences": "Vizualinės nuostatos",
"Player style: ": "Vaizdo grotuvo stilius: ",
"Dark mode: ": "Tamsus rėžimas: ",
"Theme: ": "Tema: ",
"dark": "tamsi",
"light": "šviesi",
"Thin mode: ": "Sugretintas rėžimas: ",
"Miscellaneous preferences": "Įvairios nuostatos",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Automatinis šaltinio nukreipimas (atsarginis nukreipimas į redirect.Invidous.io): ",
"Subscription preferences": "Prenumeratų nuostatos",
"Show annotations by default for subscribed channels: ": "Prenumeruojamiems kanalams subtitrus rodyti pagal nutylėjimą: ",
"Redirect homepage to feed: ": "Peradresuoti pagrindinį puslapį į kanalų sąrašą: ",
"Number of videos shown in feed: ": "Vaizdo įrašų kiekis kanalų sąraše: ",
"Sort videos by: ": "Rūšiuoti vaizdo įrašus pagal: ",
"published": "paskelbta",
"published - reverse": "paskelbta - atvirkštine tvarka",
"alphabetically": "pagal abėcėlę",
"alphabetically - reverse": "pagal abėcėlę - atvirkštine tvarka",
"channel name": "kanalo pavadinimas",
"channel name - reverse": "kanalo pavadinimas - atvirkštine tvarka",
"Only show latest video from channel: ": "Rodyti tik naujausius vaizdo įrašus iš kanalo: ",
"Only show latest unwatched video from channel: ": "Rodyti tik naujausius nežiūrėtus vaizdo įrašus iš kanalo: ",
"Only show unwatched: ": "Rodyti tik nežiūrėtus: ",
"Only show notifications (if there are any): ": "Rodyti tik pranešimus (jei yra): ",
"Enable web notifications": "Įgalinti žiniatinklio pranešimus",
"`x` uploaded a video": "`x` įkėlė vaizdo įrašą",
"`x` is live": "`x` transliuoja tiesiogiai",
"Data preferences": "Duomenų parinktys",
"Clear watch history": "Išvalyti žiūrėjimo istoriją",
"Import/export data": "Importuoti/ eksportuoti duomenis",
"Change password": "Pakeisti slaptažodį",
"Manage subscriptions": "Valdyti prenumeratas",
"Manage tokens": "Valdyti žetonus",
"Watch history": "Žiūrėjimo istorija",
"Delete account": "Ištrinti paskyrą",
"Administrator preferences": "Administratoriaus nuostatos",
"Default homepage: ": "Numatytasis pagrindinis puslapis ",
"Feed menu: ": "Kanalų sąrašo meniu: ",
"Show nickname on top: ": "Rodyti slapyvardį viršuje: ",
"Top enabled: ": "Įgalinti viršų: ",
"CAPTCHA enabled: ": "Įgalinta CAPTCHA: ",
"Login enabled: ": "Įgalintas prisijungimas: ",
"Registration enabled: ": "Įgalinta registracija: ",
"Report statistics: ": "Dalintis statistika: ",
"Save preferences": "Išsaugoti nuostatas",
"Subscription manager": "Prenumeratų valdytojas",
"Token manager": "Žetonų valdytojas",
"Token": "Žetonas",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` prenumerata",
"": "`x` prenumeratos"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` žetonas",
"": "`x` žetonai"
},
"Import/export": "Importuoti/ eksportuoti",
"unsubscribe": "atšaukti prenumeratą",
"revoke": "atšaukti",
"Subscriptions": "Prenumeratos",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` nematytas pranešimas",
"": "`x` nematyti pranešimai"
},
"search": "ieškoti",
"Log out": "Atsijungti",
"Released under the AGPLv3 on Github.": "Išleista pagal AGPLv3 licenciją Github.",
"Source available here.": "Kodas prieinamas čia.",
"View JavaScript license information.": "Žiūrėti JavaScript licencijos informaciją.",
"View privacy policy.": "Žiūrėti privatumo politiką.",
"Trending": "Tendencijos",
"Public": "Viešas",
"Unlisted": "Neįtrauktas į sąrašą",
"Private": "Neviešas",
"View all playlists": "Žiūrėti visus grojaraščius",
"Updated `x` ago": "Atnaujinta prieš `x`",
"Delete playlist `x`?": "Ištrinti grojaraštį `x`?",
"Delete playlist": "Ištrinti grojaraštį",
"Create playlist": "Sukurti grojaraštį",
"Title": "Pavadinimas",
"Playlist privacy": "Grojaraščio privatumas",
"Editing playlist `x`": "Redaguojamas grojaraštis `x`",
"Show more": "Rodyti daugiau",
"Show less": "Rodyti mažiau",
"Watch on YouTube": "Žiaurėti Youtube",
"Switch Invidious Instance": "Keisti Invidious šaltinį",
"Broken? Try another Invidious Instance": "Neveikia? Bandyk kitą Invidious šaltinį",
"Hide annotations": "Slėpti anotacijas",
"Show annotations": "Rodyti anotacijas",
"Genre: ": "Žanras: ",
"License: ": "Licencija: ",
"Family friendly? ": "Draugiška šeimai? ",
"Wilson score: ": "Wilson taškai: ",
"Engagement: ": "Įsitraukimas: ",
"Whitelisted regions: ": "Prieinantys regionai: ",
"Blacklisted regions: ": "Blokuojami regionai: ",
"Shared `x`": "Pasidalino `x`",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` peržiūrų",
"": "`x` peržiūrų"
},
"Premieres in `x`": "Premjera už `x`",
"Premieres `x`": "Premjera`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.": "Sveiki! Atrodo, kad turite išjungę \"JavaScript\". Spauskite čia norėdami peržiūrėti komentarus, turėkite omenyje, kad jų įkėlimas gali užtrukti.",
"View YouTube comments": "Žiūrėti YouTube komentarus",
"View more comments on Reddit": "Žiūrėti daugiau komentarų Reddit",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Žiūrėti `x` komentarus",
"": "Žiūrėti `x` komentarus"
},
"View Reddit comments": "Žiūrėti Reddit komentarus",
"Hide replies": "Slėpti atsakymus",
"Show replies": "Rodyti atsakymus",
"Incorrect password": "Slaptažodis neteisingas",
"Quota exceeded, try again in a few hours": "Viršyta kvota, bandykite dar kartą po keleto valandų",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Nepavyko prisijungti, įsitikinkite, kad yra įjungta dviejų etapų autentifikacija (Autentifikatorius arba SMS).",
"Invalid TFA code": "Neteisingas TFA kodas",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Prisijungimas nepavyko. Tai gali būti todėl, kad jūsų paskyroje nėra įjungta dviejų etapų autentifikacija.",
"Wrong answer": "Atsakymas neteisingas",
"Erroneous CAPTCHA": "Klaidinga CAPTCHA",
"CAPTCHA is a required field": "CAPTCHA yra reikalinga šiam laukeliui",
"User ID is a required field": "Vartotojo ID yra reikalingas šiam laukeliui",
"Password is a required field": "Slaptažodis yra reikalingas šiam laukeliui",
"Wrong username or password": "Neteisingas vartotojo vardas arba slaptažodis",
"Please sign in using 'Log in with Google'": "Prašome prisijungti naudojant \"Prisijungti su\" Google \"",
"Password cannot be empty": "Slaptažodžio laukelis negali būti tuščias",
"Password cannot be longer than 55 characters": "Slaptažodis negali būti ilgesnis nei 55 simboliai",
"Please log in": "Prašome prisijungti",
"Invidious Private Feed for `x`": "Invidious neviešas kanalų sąrašas `x`",
"channel:`x`": "kanalas:`x`",
"Deleted or invalid channel": "Panaikintas arba netinkamas kanalas",
"This channel does not exist.": "Šis kanalas neegzistuoja.",
"Could not get channel info.": "Nepavyko gauti kanalo informacijos.",
"Could not fetch comments": "Nepavyko atsiųsti komentarų",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Žiūrėti `x` atsakymus",
"": "Žiūrėti `x` atsakymus"
},
"`x` ago": "`x` prieš",
"Load more": "Pakrauti daugiau",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` taškai",
"": "`x` taškai"
},
"Could not create mix.": "Nepavyko sukurti derinio.",
"Empty playlist": "Tuščias grojaraštis",
"Not a playlist.": "Ne grojaraštis.",
"Playlist does not exist.": "Grojaraštis neegzistuoja.",
"Could not pull trending pages.": "Nepavyko ištraukti tendencijų puslapių.",
"Hidden field \"challenge\" is a required field": "Paslėptas laukas „iššūkis“ yra privalomas laukas",
"Hidden field \"token\" is a required field": "Paslėptas laukas „žetonas“ yra privalomas laukas",
"Erroneous challenge": "Klaidingas iššūkis",
"Erroneous token": "Klaidingas žetonas",
"No such user": "Nėra tokio vartotojo",
"Token is expired, please try again": "Žetonas pasibaigęs, prašome bandyti dar kartą",
"English": "Anglų",
"English (auto-generated)": "Anglų (Sugeneruota automatiškai)",
"Afrikaans": "Afrikans",
"Albanian": "Albanų",
"Amharic": "Amharų",
"Arabic": "Arabų",
"Armenian": "Armėnų",
"Azerbaijani": "Azerbaidžanų",
"Bangla": "Bengalų",
"Basque": "Baskų",
"Belarusian": "Baltarusių",
"Bosnian": "Bosnių",
"Bulgarian": "Bulgarų",
"Burmese": "Birmiečių",
"Catalan": "Katalonų",
"Cebuano": "Cebuano",
"Chinese (Simplified)": "Kinų (supaprastinta)",
"Chinese (Traditional)": "Kinų (tradicinė)",
"Corsican": "Korsikiečių",
"Croatian": "Kroatų",
"Czech": "Čekų",
"Danish": "Danų",
"Dutch": "Nyderlandų",
"Esperanto": "Esperanto",
"Estonian": "Estų",
"Filipino": "Filipiniečių",
"Finnish": "Suomių",
"French": "Prancūzų",
"Galician": "Galicijos",
"Georgian": "Sakartveliečių",
"German": "Vokiečių",
"Greek": "Graikų",
"Gujarati": "Gujarati",
"Haitian Creole": "Haičio kreolė",
"Hausa": "Hausa",
"Hawaiian": "Havajiečių",
"Hebrew": "Hebrajų",
"Hindi": "Hindi",
"Hmong": "Hmong",
"Hungarian": "Vengrų",
"Icelandic": "Islandų",
"Igbo": "Igbo",
"Indonesian": "Indoneziečių",
"Irish": "Airių",
"Italian": "Italų",
"Japanese": "Japonų",
"Javanese": "Javos",
"Kannada": "Kannada",
"Kazakh": "Kazachų",
"Khmer": "Khmerų",
"Korean": "Korejiėčių",
"Kurdish": "Kurdų",
"Kyrgyz": "Kirgizų",
"Lao": "Lao",
"Latin": "Lotynų",
"Latvian": "Latvių",
"Lithuanian": "Lietuvių",
"Luxembourgish": "Liuksemburgiečių",
"Macedonian": "Šiaurės makedonų",
"Malagasy": "Malagasi",
"Malay": "Malajų",
"Malayalam": "Malayalam",
"Maltese": "Maltiečių",
"Maori": "Maori",
"Marathi": "Marathi",
"Mongolian": "Mongolų",
"Nepali": "Nepaliečių",
"Norwegian Bokmål": "Norvegų Bokmål",
"Nyanja": "Nyanja",
"Pashto": "Paštunų",
"Persian": "Persų",
"Polish": "Lenkų",
"Portuguese": "Portugalų",
"Punjabi": "Punjabi",
"Romanian": "Romėnų",
"Russian": "Rusų",
"Samoan": "Samoa",
"Scottish Gaelic": "Škotų Gaelic",
"Serbian": "Serbų",
"Shona": "Shona",
"Sindhi": "Sindhi",
"Sinhala": "Sinhala",
"Slovak": "Slovakų",
"Slovenian": "Slovėnų",
"Somali": "Somaliečių",
"Southern Sotho": "Pietų Sotho",
"Spanish": "Ispanų",
"Spanish (Latin America)": "Ispanų (Lotynų Amerika)",
"Sundanese": "Sudaniečių",
"Swahili": "Svahili",
"Swedish": "Švedų",
"Tajik": "Tadžikų",
"Tamil": "Tamilų",
"Telugu": "Telugų",
"Thai": "Talaindiečių",
"Turkish": "Turkų",
"Ukrainian": "Ukrainiečių",
"Urdu": "Udrų",
"Uzbek": "Uzbekų",
"Vietnamese": "Vietnamiečių",
"Welsh": "Velso",
"Western Frisian": "Vakarų Fryzų",
"Xhosa": "Xhosa",
"Yiddish": "Jidiš",
"Yoruba": "Yorubiečių",
"Zulu": "Zulu",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` metus",
"": "`x` metus"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` mėnesį",
"": "`x` mėnesius"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` savaitę",
"": "`x` savaites"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dieną",
"": "`x` dienas"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` valandą",
"": "`x` valandas"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minutę",
"": "`x` minutes"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` sekundę",
"": "`x` sekundes"
},
"Fallback comments: ": "Atsarginiai komentarai: ",
"Popular": "Populiaru",
"Search": "Paieška",
"Top": "Top",
"About": "Apie",
"Rating: ": "Reitingas: ",
"Language: ": "Kalba: ",
"View as playlist": "Žiūrėti kaip grojaraštį",
"Default": "Numatytasis",
"Music": "Muzika",
"Gaming": "Žaidimai",
"News": "Naujienos",
"Movies": "Filmai",
"Download": "Atsisiųsti",
"Download as: ": "Atsisiųsti kaip: ",
"%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "(redaguota)",
"YouTube comment permalink": "YouTube komentaro adresas",
"permalink": "adresas",
"`x` marked it with a ❤": "`x` pažymėjo tai su ❤",
"Audio mode": "Garso rėžimas",
"Video mode": "Vaizdo rėžimas",
"Videos": "Vaizdo įrašai",
"Playlists": "Grojaraiščiai",
"Community": "Bendruomenė",
"relevance": "Aktualumas",
"rating": "Reitingas",
"date": "Įkėlimo data",
"views": "Peržiūrų skaičius",
"content_type": "Tipas",
"duration": "Trukmė",
"features": "Funkcijos",
"sort": "Rūšiuoti pagal",
"hour": "Per paskutinę valandą",
"today": "Šiandien",
"week": "Šią savaitę",
"month": "Šį mėnesį",
"year": "Šiais metais",
"video": "Vaizdo įrašas",
"channel": "Kanalas",
"playlist": "Grojaraštis",
"movie": "Filmas",
"show": "Serialas",
"hd": "HD",
"subtitles": "Subtitrai/CC",
"creative_commons": "Creative Commons",
"3d": "3D",
"live": "Tiesiogiai",
"4k": "4K",
"location": "Vietovė",
"hdr": "HDR",
"filter": "Filtras",
"Current version: ": "Dabartinė versija: ",
"next_steps_error_message": "Po to turėtumėte pabandyti: ",
"next_steps_error_message_refresh": "Atnaujinti",
"next_steps_error_message_go_to_youtube": "Eiti į YouTube"
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` abonnenter",
"`x` videos": "`x` videoer",
"`x` playlists": "`x` spillelister",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnenter",
"": "`x` abonnenter"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videoer",
"": "`x` videoer"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` spillelister",
"": "`x` spillelister"
},
"LIVE": "SANNTIDSVISNING",
"Shared `x` ago": "Delt for `x` siden",
"Unsubscribe": "Opphev abonnement",
@ -68,6 +77,8 @@
"Fallback captions: ": "Tilbakefallsundertitler: ",
"Show related videos: ": "Vis relaterte videoer? ",
"Show annotations by default: ": "Vis merknader som forvalg? ",
"Automatically extend video description: ": "Utvid videobeskrivelse automatisk: ",
"Interactive 360 degree videos: ": "Interaktive 360-gradersfilmer: ",
"Visual preferences": "Visuelle innstillinger",
"Player style: ": "Avspillerstil: ",
"Dark mode: ": "Mørk drakt: ",
@ -75,6 +86,8 @@
"dark": "Mørk",
"light": "Lys",
"Thin mode: ": "Tynt modus: ",
"Miscellaneous preferences": "Ulike innstillinger",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Automatisk instansomdirigering (faller tilbake til redirect.invidious.io): ",
"Subscription preferences": "Abonnementsinnstillinger",
"Show annotations by default for subscribed channels: ": "Vis merknader som forvalg for kanaler det abonneres på? ",
"Redirect homepage to feed: ": "Videresend hjemmeside til kilde: ",
@ -104,6 +117,7 @@
"Administrator preferences": "Administratorinnstillinger",
"Default homepage: ": "Forvalgt hjemmeside: ",
"Feed menu: ": "Kilde-meny: ",
"Show nickname on top: ": "Vis kallenavn på toppen: ",
"Top enabled: ": "Topp påskrudd? ",
"CAPTCHA enabled: ": "CAPTCHA påskrudd? ",
"Login enabled: ": "Innlogging påskrudd? ",
@ -113,16 +127,25 @@
"Subscription manager": "Abonnementsbehandler",
"Token manager": "Symbolbehandler",
"Token": "Symbol",
"`x` subscriptions": "`x` abonnementer",
"`x` tokens": "`x` symboler",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnementer",
"": "`x` abonnementer"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` symboler",
"": "`x` symboler"
},
"Import/export": "Importer/eksporter",
"unsubscribe": "opphev abonnement",
"revoke": "tilbakekall",
"Subscriptions": "Abonnement",
"`x` unseen notifications": "`x` usette merknader",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` usette merknader",
"": "`x` usette merknader"
},
"search": "søk",
"Log out": "Logg ut",
"Released under the AGPLv3 by Omar Roth.": "Utgitt med AGPLv3+lisens av Omar Roth.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Kildekode tilgjengelig her.",
"View JavaScript license information.": "Vis JavaScript-lisensinfo.",
"View privacy policy.": "Vis personvernspraksis.",
@ -138,7 +161,11 @@
"Title": "Tittel",
"Playlist privacy": "Vern av spilleliste",
"Editing playlist `x`": "Endre spilleliste «x»",
"Show more": "Vis mer",
"Show less": "Vis mindre",
"Watch on YouTube": "Vis video på YouTube",
"Switch Invidious Instance": "Bytt Invidious-instans",
"Broken? Try another Invidious Instance": "Knekt? Forsøk en annen Invidious-instans",
"Hide annotations": "Skjul merknader",
"Show annotations": "Vis merknader",
"Genre: ": "Sjanger: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Hvitlistede regioner: ",
"Blacklisted regions: ": "Svartelistede regioner: ",
"Shared `x`": "Delt `x`",
"`x` views": "`x` visninger",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` visninger",
"": "`x` visninger"
},
"Premieres in `x`": "Premiere om `x`",
"Premieres `x`": "Première `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.": "Hei. Det ser ut til at du har JavaScript avslått. Klikk her for å vise kommentarer, ha i minnet at innlasting tar lengre tid.",
"View YouTube comments": "Vis YouTube-kommentarer",
"View more comments on Reddit": "Vis flere kommenterer på Reddit",
"View `x` comments": "Vis `x` kommentarer",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` kommentarer",
"": "Vis `x` kommentarer"
},
"View Reddit comments": "Vis Reddit-kommentarer",
"Hide replies": "Skjul svar",
"Show replies": "Vis svar",
@ -180,10 +213,16 @@
"This channel does not exist.": "Denne kanalen finnes ikke.",
"Could not get channel info.": "Kunne ikke innhente kanalinfo.",
"Could not fetch comments": "Kunne ikke hente kommentarer",
"View `x` replies": "Vis `x` svar",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` svar",
"": "Vis `x` svar"
},
"`x` ago": "`x` siden",
"Load more": "Last inn flere",
"`x` points": "`x` poeng",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` poeng",
"": "`x` poeng"
},
"Could not create mix.": "Kunne ikke opprette miks.",
"Empty playlist": "Spillelisten er tom",
"Not a playlist.": "Ugyldig spilleliste.",
@ -301,15 +340,37 @@
"Yiddish": "Jiddisk",
"Yoruba": "Joruba",
"Zulu": "Zulu",
"`x` years": "`x` år",
"`x` months": "`x` måneder",
"`x` weeks": "`x` uker",
"`x` days": "`x` dager",
"`x` hours": "`x` timer",
"`x` minutes": "`x` minutter",
"`x` seconds": "`x` sekunder",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` år",
"": "`x` år"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` måneder",
"": "`x` måneder"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` uker",
"": "`x` uker"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dager",
"": "`x` dager"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` timer",
"": "`x` timer"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minutter",
"": "`x` minutter"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` sekunder",
"": "`x` sekunder"
},
"Fallback comments: ": "Tilbakefallskommentarer: ",
"Popular": "Populært",
"Search": "Søk",
"Top": "Topp",
"About": "Om",
"Rating: ": "Vurdering: ",
@ -332,5 +393,35 @@
"Videos": "Videoer",
"Playlists": "Spillelister",
"Community": "Gemenskap",
"Current version: ": "Gjeldende versjon: "
"relevance": "relevans",
"rating": "vurdering",
"date": "dato",
"views": "visninger",
"content_type": "innholdstype",
"duration": "varighet",
"features": "funksjoner",
"sort": "sorter",
"hour": "time",
"today": "i dag",
"week": "uke",
"month": "måned",
"year": "år",
"video": "video",
"channel": "kanal",
"playlist": "spilleliste",
"movie": "film",
"show": "vis",
"hd": "HD",
"subtitles": "undertekster",
"creative_commons": "Creative Commons",
"3d": "3D",
"live": "direkte",
"4k": "4k",
"location": "sted",
"hdr": "HDR",
"filter": "filtrer",
"Current version: ": "Gjeldende versjon: ",
"next_steps_error_message": "Etterpå bør du prøve dette: ",
"next_steps_error_message_refresh": "Gjenoppfrisk",
"next_steps_error_message_go_to_youtube": "Gå til YouTube"
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` abonnees",
"`x` videos": "`x` video's",
"`x` playlists": "`x` afspeellijsten",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnees",
"": "`x` abonnees"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` video's",
"": "`x` video's"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` afspeellijsten",
"": "`x` afspeellijsten"
},
"LIVE": "LIVE",
"Shared `x` ago": "Gedeeld: `x` geleden",
"Unsubscribe": "Deabonneren",
@ -68,6 +77,8 @@
"Fallback captions: ": "Alternatieve ondertiteling: ",
"Show related videos: ": "Gerelateerde video's tonen? ",
"Show annotations by default: ": "Standaard annotaties tonen? ",
"Automatically extend video description: ": "Breid videobeschrijving automatisch uit: ",
"Interactive 360 degree videos: ": "Interactieve 360-graden-video's ",
"Visual preferences": "Visuele instellingen",
"Player style: ": "Speler vormgeving ",
"Dark mode: ": "Donkere modus: ",
@ -75,6 +86,8 @@
"dark": "donker",
"light": "licht",
"Thin mode: ": "Smalle modus: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Abonnementsinstellingen",
"Show annotations by default for subscribed channels: ": "Standaard annotaties tonen voor geabonneerde kanalen? ",
"Redirect homepage to feed: ": "Startpagina omleiden naar feed: ",
@ -104,6 +117,7 @@
"Administrator preferences": "Beheerdersinstellingen",
"Default homepage: ": "Standaard startpagina: ",
"Feed menu: ": "Feedmenu: ",
"Show nickname on top: ": "",
"Top enabled: ": "Bovenkant inschakelen? ",
"CAPTCHA enabled: ": "CAPTCHA gebruiken? ",
"Login enabled: ": "Inloggen toestaan? ",
@ -113,16 +127,25 @@
"Subscription manager": "Abonnementen beheren",
"Token manager": "Toegangssleutels beheren",
"Token": "Toegangssleutel",
"`x` subscriptions": "`x` abonnementen",
"`x` tokens": "`x` toegangssleutels",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnementen",
"": "`x` abonnementen"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` toegangssleutels",
"": "`x` toegangssleutels"
},
"Import/export": "Importeren/Exporteren",
"unsubscribe": "Deabonneren",
"revoke": "Intrekken",
"Subscriptions": "Abonnementen",
"`x` unseen notifications": "`x` ongelezen meldingen",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ongelezen meldingen",
"": "`x` ongelezen meldingen"
},
"search": "zoeken",
"Log out": "Uitloggen",
"Released under the AGPLv3 by Omar Roth.": "Uitgebracht onder de AGPLv3-licentie, door Omar Roth.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "De broncode is hier beschikbaar.",
"View JavaScript license information.": "JavaScript-licentieinformatie tonen.",
"View privacy policy.": "Privacybeleid tonen.",
@ -138,7 +161,11 @@
"Title": "Titel",
"Playlist privacy": "Afspeellijst privacy",
"Editing playlist `x`": "Afspeellijst `x` wijzigen",
"Show more": "Toon meer",
"Show less": "Toon minder",
"Watch on YouTube": "Video bekijken op YouTube",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "Annotaties verbergen",
"Show annotations": "Annotaties tonen",
"Genre: ": "Genre: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Toegestane regio's: ",
"Blacklisted regions: ": "Geblokkeerde regio's: ",
"Shared `x`": "`x` gedeeld",
"`x` views": "`x` weergaven",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` weergaven",
"": "`x` weergaven"
},
"Premieres in `x`": "Verschijnt over `x`",
"Premieres `x`": "Verschijnt op `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.": "Hoi! Het lijkt erop dat je JavaScript hebt uitgeschakeld. Klik hier om de reacties te bekijken. Let op: het laden duurt wat langer.",
"View YouTube comments": "YouTube-reacties tonen",
"View more comments on Reddit": "Meer reacties bekijken op Reddit",
"View `x` comments": "`x` reacties tonen",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` reacties tonen",
"": "`x` reacties tonen"
},
"View Reddit comments": "Reddit-reacties tonen",
"Hide replies": "Antwoorden verbergen",
"Show replies": "Antwoorden tonen",
@ -180,10 +213,16 @@
"This channel does not exist.": "Dit kanaal bestaat niet.",
"Could not get channel info.": "Kan geen kanaalinformatie ophalen.",
"Could not fetch comments": "Kan reacties niet ophalen",
"View `x` replies": "`x` antwoorden tonen",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` antwoorden tonen",
"": "`x` antwoorden tonen"
},
"`x` ago": "`x` geleden",
"Load more": "Meer laden",
"`x` points": "`x` punten",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` punten",
"": "`x` punten"
},
"Could not create mix.": "Kan geen mix maken.",
"Empty playlist": "Lege afspeellijst",
"Not a playlist.": "Ongeldige afspeellijst.",
@ -301,15 +340,37 @@
"Yiddish": "Joods",
"Yoruba": "Yoruba",
"Zulu": "Zulu",
"`x` years": "`x` jaar",
"`x` months": "`x` maanden",
"`x` weeks": "`x` weken",
"`x` days": "`x` dagen",
"`x` hours": "`x` uur",
"`x` minutes": "`x` minuten",
"`x` seconds": "`x` seconden",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` jaar",
"": "`x` jaren"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` maanden",
"": "`x` maanden"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` weken",
"": "`x` weken"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dagen",
"": "`x` dagen"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` uur",
"": "`x` uren"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuten",
"": "`x` minuten"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` seconden",
"": "`x` seconden"
},
"Fallback comments: ": "Terugvallen op ",
"Popular": "Populair",
"Search": "Zoeken",
"Top": "Top",
"About": "Over",
"Rating: ": "Waardering: ",
@ -332,6 +393,35 @@
"Videos": "Video's",
"Playlists": "Afspeellijsten",
"Community": "Gemeenschap",
"relevance": "relevantie",
"rating": "beoordeling",
"date": "datum",
"views": "keren bekeken",
"content_type": "Type inhoud",
"duration": "duur",
"features": "eigenschappen",
"sort": "sorteren",
"hour": "uur",
"today": "vandaag",
"week": "week",
"month": "maand",
"year": "jaar",
"video": "video",
"channel": "kanaal",
"playlist": "afspeellijst",
"movie": "film",
"show": "show",
"hd": "HD",
"subtitles": "ondertitels",
"creative_commons": "Creative Commons",
"3d": "3D",
"live": "Live",
"4k": "4K",
"location": "locatie",
"hdr": "HDR",
"filter": "verfijnen",
"Current version: ": "Huidige versie: ",
"Download is disabled.": "Downloaden is uitgeschakeld."
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` subskrybcji",
"`x` videos": "`x` filmów",
"`x` playlists": "`x` playlist",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` subskrybcji",
"": "`x` subskrybcji"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` filmów",
"": "`x` filmów"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` playlist",
"": "`x` playlist"
},
"LIVE": "NA ŻYWO",
"Shared `x` ago": "Udostępniono `x` temu",
"Unsubscribe": "Odsubskrybuj",
@ -62,12 +71,14 @@
"Preferred video quality: ": "Preferowana jakość filmów: ",
"Player volume: ": "Głośność odtwarzacza: ",
"Default comments: ": "Domyślne komentarze: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Domyślne napisy: ",
"Fallback captions: ": "Zastępcze napisy: ",
"Show related videos: ": "Pokaż powiązane filmy? ",
"Show annotations by default: ": "Domyślnie pokazuj adnotacje: ",
"Automatically extend video description: ": "Automatycznie rozwijaj opisy filmów: ",
"Interactive 360 degree videos: ": "Interaktywne filmy 360 stopni: ",
"Visual preferences": "Preferencje Wizualne",
"Player style: ": "Styl odtwarzacza: ",
"Dark mode: ": "Ciemny motyw: ",
@ -75,6 +86,8 @@
"dark": "ciemny",
"light": "jasny",
"Thin mode: ": "Tryb minimalny: ",
"Miscellaneous preferences": "Różne preferencje",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Automatyczne przekierowanie instancji (powrót do redirect.invidious.io): ",
"Subscription preferences": "Preferencje subskrybcji",
"Show annotations by default for subscribed channels: ": "Domyślnie wyświetlaj adnotacje dla subskrybowanych kanałów: ",
"Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ",
@ -103,7 +116,8 @@
"Delete account": "Usuń konto",
"Administrator preferences": "Preferencje administratora",
"Default homepage: ": "Domyślna strona główna: ",
"Feed menu: ": "",
"Feed menu: ": "Menu aktualności ",
"Show nickname on top: ": "Pokaż pseudonim na górze: ",
"Top enabled: ": "\"Top\" aktywne: ",
"CAPTCHA enabled: ": "CAPTCHA aktywna? ",
"Login enabled: ": "Logowanie włączone? ",
@ -113,16 +127,25 @@
"Subscription manager": "Manager subskrybcji",
"Token manager": "Menedżer tokenów",
"Token": "Token",
"`x` subscriptions": "`x` subskrybcji",
"`x` tokens": "",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` subskrybcji",
"": "`x` subskrybcji"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` token",
"": "`x` tokenów"
},
"Import/export": "Import/Eksport",
"unsubscribe": "odsubskrybuj",
"revoke": "cofnij",
"Subscriptions": "Subskrybcje",
"`x` unseen notifications": "`x` nowych powiadomień",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` nowych powiadomień",
"": "`x` nowych powiadomień"
},
"search": "szukaj",
"Log out": "Wyloguj",
"Released under the AGPLv3 by Omar Roth.": "Wydano na licencji AGPLv3 przez Omar Roth.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Kod źródłowy dostępny tutaj.",
"View JavaScript license information.": "Wyświetl informację o licencji JavaScript.",
"View privacy policy.": "Polityka prywatności.",
@ -138,7 +161,11 @@
"Title": "Tytuł",
"Playlist privacy": "Widoczność playlisty",
"Editing playlist `x`": "Edycja playlisty `x`",
"Show more": "Pokaż więcej",
"Show less": "Pokaż mniej",
"Watch on YouTube": "Zobacz film na YouTube",
"Switch Invidious Instance": "Przełącz instancję Invidious",
"Broken? Try another Invidious Instance": "Nie działa? Spróbuj innej instancji Invidious",
"Hide annotations": "Ukryj adnotacje",
"Show annotations": "Pokaż adnotacje",
"Genre: ": "Gatunek: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Dostępny na obszarach: ",
"Blacklisted regions: ": "Niedostępny na obszarach: ",
"Shared `x`": "Udostępniono `x`",
"`x` views": "`x` wyświetleń",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` wyświetleń",
"": "`x` wyświetleń"
},
"Premieres in `x`": "Publikacja za `x`",
"Premieres `x`": "Publikacja za `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.": "Cześć! Wygląda na to, że masz wyłączoną obsługę JavaScriptu. Kliknij tutaj, żeby zobaczyć komentarze. Pamiętaj, że wczytywanie może potrwać dłużej.",
"View YouTube comments": "Wyświetl komentarze z YouTube",
"View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie",
"View `x` comments": "Wyświetl `x` komentarzy",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Wyświetl `x` komentarzy",
"": "Wyświetl `x` komentarzy"
},
"View Reddit comments": "Wyświetl komentarze z Redditta",
"Hide replies": "Ukryj odpowiedzi",
"Show replies": "Pokaż odpowiedzi",
@ -174,16 +207,22 @@
"Password cannot be empty": "Hasło nie może być puste",
"Password cannot be longer than 55 characters": "Hasło nie może być dłuższe niż 55 znaków",
"Please log in": "Proszę się zalogować",
"Invidious Private Feed for `x`": "",
"Invidious Private Feed for `x`": "Prywatne aktualności dla `x`",
"channel:`x`": "kanał:`x",
"Deleted or invalid channel": "Usunięty lub niepoprawny kanał",
"This channel does not exist.": "Ten kanał nie istnieje.",
"Could not get channel info.": "Nie udało się uzyskać informacji o kanale.",
"Could not fetch comments": "Nie udało się pobrać komentarzy",
"View `x` replies": "Wyświetl `x` odpowiedzi",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Wyświetl `x` odpowiedzi",
"": "Wyświetl `x` odpowiedzi"
},
"`x` ago": "`x` temu",
"Load more": "Wczytaj więcej",
"`x` points": "`x` punktów",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` punktów",
"": "`x` punktów"
},
"Could not create mix.": "Nie udało się utworzyć miksu.",
"Empty playlist": "Lista odtwarzania jest pusta",
"Not a playlist.": "Niepoprawna lista.",
@ -301,15 +340,37 @@
"Yiddish": "jidysz",
"Yoruba": "joruba",
"Zulu": "zuluski",
"`x` years": "`x` lat",
"`x` months": "`x` miesięcy",
"`x` weeks": "`x` tygodni",
"`x` days": "`x` dni",
"`x` hours": "`x` godzin",
"`x` minutes": "`x` minut",
"`x` seconds": "`x` sekund",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` lat",
"": "`x` lat"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` miesięcy",
"": "`x` miesięcy"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tygodni",
"": "`x` tygodni"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dni",
"": "`x` dni"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` godzin",
"": "`x` godzin"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minut",
"": "`x` minut"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` sekund",
"": "`x` sekund"
},
"Fallback comments: ": "Zastępcze komentarze: ",
"Popular": "Popularne",
"Search": "Szukaj",
"Top": "Top",
"About": "Informacje",
"Rating: ": "Ocena: ",
@ -332,5 +393,35 @@
"Videos": "Filmy",
"Playlists": "Playlisty",
"Community": "Społeczność",
"Current version: ": "Aktualna wersja: "
"relevance": "Trafność",
"rating": "Ocena",
"date": "data",
"views": "Liczba wyświetleń",
"content_type": "Typ",
"duration": "Długość",
"features": "Funkcje",
"sort": "sortuj",
"hour": "godzina",
"today": "dzisiaj",
"week": "tydzień",
"month": "miesiąc",
"year": "rok",
"video": "Film",
"channel": "kanał",
"playlist": "playlista",
"movie": "film",
"show": "pokaż",
"hd": "hd",
"subtitles": "napisy",
"creative_commons": "creative_commons",
"3d": "3d",
"live": "Na żywo",
"4k": "4k",
"location": "Lokalizacja",
"hdr": "hdr",
"filter": "filtr",
"Current version: ": "Aktualna wersja: ",
"next_steps_error_message": "Po czym powinien*ś spróbować: ",
"next_steps_error_message_refresh": "Odśwież",
"next_steps_error_message_go_to_youtube": "Przejdź do YouTube"
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` inscritos",
"`x` videos": "`x` vídeos",
"`x` playlists": "`x` listas de reprodução",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` inscritos",
"": "`x` inscritos"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` vídeos",
"": "`x` vídeos"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` listas de reprodução",
"": "`x` listas de reprodução"
},
"LIVE": "AO VIVO",
"Shared `x` ago": "Compartilhado `x` atrás",
"Unsubscribe": "Cancelar inscrição",
@ -62,12 +71,14 @@
"Preferred video quality: ": "Qualidade de vídeo preferida: ",
"Player volume: ": "Volume de reprodução: ",
"Default comments: ": "Preferência de comentários: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Preferência de legendas: ",
"Fallback captions: ": "Legendas alternativas: ",
"Show related videos: ": "Mostrar vídeos relacionados: ",
"Show annotations by default: ": "Sempre mostrar anotações: ",
"Automatically extend video description: ": "Estenda automaticamente a descrição do vídeo: ",
"Interactive 360 degree videos: ": "Vídeos interativos de 360 graus: ",
"Visual preferences": "Preferências visuais",
"Player style: ": "Estilo do tocador: ",
"Dark mode: ": "Modo escuro: ",
@ -75,6 +86,8 @@
"dark": "escuro",
"light": "claro",
"Thin mode: ": "Modo compacto: ",
"Miscellaneous preferences": "Preferências diversas",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Redirecionamento de instância automática (fallback para redirect.invidious.io): ",
"Subscription preferences": "Preferências de inscrições",
"Show annotations by default for subscribed channels: ": "Sempre mostrar anotações dos vídeos de canais inscritos: ",
"Redirect homepage to feed: ": "Redirecionar página inicial para o feed: ",
@ -104,6 +117,7 @@
"Administrator preferences": "Preferências de administrador",
"Default homepage: ": "Página de início padrão: ",
"Feed menu: ": "Menu do feed: ",
"Show nickname on top: ": "Mostrar o nickname no topo: ",
"Top enabled: ": "Habilitar destaques: ",
"CAPTCHA enabled: ": "Habilitar CAPTCHA: ",
"Login enabled: ": "Habilitar login: ",
@ -113,16 +127,25 @@
"Subscription manager": "Gerenciador de inscrições",
"Token manager": "Gerenciador de tokens",
"Token": "Token",
"`x` subscriptions": "`x` inscrições",
"`x` tokens": "`x` tokens",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` inscrições",
"": "`x` inscrições"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens",
"": "Símbolos `x`"
},
"Import/export": "Importar/Exportar",
"unsubscribe": "cancelar inscrição",
"revoke": "revogar",
"Subscriptions": "Inscrições",
"`x` unseen notifications": "`x` notificações não visualizadas",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificações não visualizadas",
"": "`x` notificações não visualizadas"
},
"search": "Pesquisar",
"Log out": "Sair",
"Released under the AGPLv3 by Omar Roth.": "Publicado sob a licença AGPLv3, por Omar Roth.",
"Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no Github.",
"Source available here.": "Código-fonte disponível aqui.",
"View JavaScript license information.": "Ver informações da licença do JavaScript.",
"View privacy policy.": "Ver a política de privacidade.",
@ -138,7 +161,11 @@
"Title": "Título",
"Playlist privacy": "Privacidade da playlist",
"Editing playlist `x`": "Editando playlist `x`",
"Show more": "Mostrar mais",
"Show less": "Mostrar menos",
"Watch on YouTube": "Assistir no YouTube",
"Switch Invidious Instance": "Mudar a instância do Invidious",
"Broken? Try another Invidious Instance": "Quebrou? Tente outra Instância do Invidious",
"Hide annotations": "Ocultar anotações",
"Show annotations": "Mostrar anotações",
"Genre: ": "Gênero: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Regiões permitidas: ",
"Blacklisted regions: ": "Regiões bloqueadas: ",
"Shared `x`": "Compartilhado `x`",
"`x` views": "`x` visualizações",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizações",
"": "`x` visualizações"
},
"Premieres in `x`": "Estreia em `x`",
"Premieres `x`": "Estreia `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.": "Oi! Parece que seu JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar um pouco mais de tempo para carregar.",
"View YouTube comments": "Ver comentários no YouTube",
"View more comments on Reddit": "Ver mais comentários no Reddit",
"View `x` comments": "Ver `x` comentários",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários",
"": "Ver `x` comentários"
},
"View Reddit comments": "Ver comentários no Reddit",
"Hide replies": "Ocultar respostas",
"Show replies": "Mostrar respostas",
@ -180,10 +213,16 @@
"This channel does not exist.": "Este canal não existe.",
"Could not get channel info.": "Não foi possível obter as informações do canal.",
"Could not fetch comments": "Não foi possível obter os comentários",
"View `x` replies": "Ver `x` respostas",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` respostas",
"": "Ver `x` respostas"
},
"`x` ago": "`x` atrás",
"Load more": "Carregar mais",
"`x` points": "`x` pontos",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pontos",
"": "`x` pontos"
},
"Could not create mix.": "Não foi possível criar o mix.",
"Empty playlist": "Lista de reprodução vazia",
"Not a playlist.": "Não é uma lista de reprodução.",
@ -301,15 +340,37 @@
"Yiddish": "Iídiche",
"Yoruba": "Iorubá",
"Zulu": "Zulu",
"`x` years": "`x` anos",
"`x` months": "`x` meses",
"`x` weeks": "`x` semanas",
"`x` days": "`x` dias",
"`x` hours": "`x` horas",
"`x` minutes": "`x` minutos",
"`x` seconds": "`x` segundos",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ano",
"": "`x` anos"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` mês",
"": "`x` meses"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` semana",
"": "`x` semanas"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dia",
"": "`x` dia"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` hora",
"": "`x` horas"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto",
"": "`x` minutos"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` segundo",
"": "`x` segundos"
},
"Fallback comments: ": "Comentários alternativos: ",
"Popular": "Populares",
"Search": "Procurar",
"Top": "No topo",
"About": "Sobre",
"Rating: ": "Avaliação: ",
@ -332,5 +393,35 @@
"Videos": "Vídeos",
"Playlists": "Listas de reprodução",
"Community": "Comunidade",
"Current version: ": "Versão atual: "
"relevance": "relevância",
"rating": "avaliação",
"date": "data",
"views": "visualizações",
"content_type": "content_type",
"duration": "duração",
"features": "recursos",
"sort": "ordenar",
"hour": "hora",
"today": "hoje",
"week": "semana",
"month": "mês",
"year": "ano",
"video": "vídeo",
"channel": "Canal",
"playlist": "playlist",
"movie": "filme",
"show": "show",
"hd": "hd",
"subtitles": "legendas",
"creative_commons": "creative_commons",
"3d": "3d",
"live": "ao vivo",
"4k": "4k",
"location": "localização",
"hdr": "hdr",
"filter": "filtro",
"Current version: ": "Versão atual: ",
"next_steps_error_message": "Depois disso, você deve tentar: ",
"next_steps_error_message_refresh": "Atualizar",
"next_steps_error_message_go_to_youtube": "Ir para o YouTube"
}

View File

@ -1,10 +1,16 @@
{
"`x` subscribers..([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscritores..([^.,0-9]|^)1([^.,0-9]|$)",
"`x` subscribers..": "`x` subscritores.",
"`x` videos..([^.,0-9]|^)1([^.,0-9]|$)": "`x` videos..([^.,0-9]|^)1([^.,0-9]|$)",
"`x` videos..": "`x` vídeos.",
"`x` playlists..([^.,0-9]|^)1([^.,0-9]|$)": "`x` listas de reprodução.",
"`x` playlists..": "`x` listas de reprodução.",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscritores",
"": "`x` subscritores"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videos",
"": "`x` vídeos"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` listas de reprodução",
"": "`x` listas de reprodução"
},
"LIVE": "Em direto",
"Shared `x` ago": "Partilhado `x` atrás",
"Unsubscribe": "Anular subscrição",
@ -20,12 +26,12 @@
"Clear watch history?": "Limpar histórico de reprodução?",
"New password": "Nova palavra-chave",
"New passwords must match": "As novas palavra-chaves devem corresponder",
"Cannot change password for Google accounts": "Não é possível alterar a palavra-passe para contas do Google",
"Cannot change password for Google accounts": "Não é possível alterar a palavra-chave para contas do Google",
"Authorize token?": "Autorizar token?",
"Authorize token for `x`?": "Autorizar token para `x`?",
"Yes": "Sim",
"No": "Não",
"Import and Export Data": "Importar e Exportar Dados",
"Import and Export Data": "Importar e exportar dados",
"Import": "Importar",
"Import Invidious data": "Importar dados do Invidious",
"Import YouTube subscriptions": "Importar subscrições do YouTube",
@ -36,20 +42,20 @@
"Export subscriptions as OPML": "Exportar subscrições como OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar subscrições como OPML (para NewPipe e FreeTube)",
"Export data as JSON": "Exportar dados como JSON",
"Delete account?": "Apagar conta?",
"Delete account?": "Eliminar conta?",
"History": "Histórico",
"An alternative front-end to YouTube": "Uma interface alternativa ao YouTube",
"JavaScript license information": "Informação de licença do JavaScript",
"source": "código-fonte",
"Log in": "Iniciar sessão",
"Log in/register": "Iniciar sessão/Registar",
"Log in/register": "Iniciar sessão/registar",
"Log in with Google": "Iniciar sessão com o Google",
"User ID": "Utilizador",
"Password": "Palavra-chave",
"Time (h:mm:ss):": "Tempo (h:mm:ss):",
"Text CAPTCHA": "Texto CAPTCHA",
"Image CAPTCHA": "Imagem CAPTCHA",
"Sign In": "Iniciar Sessão",
"Sign In": "Iniciar sessão",
"Register": "Registar",
"E-mail": "E-mail",
"Google verification code": "Código de verificação do Google",
@ -57,7 +63,7 @@
"Player preferences": "Preferências do reprodutor",
"Always loop: ": "Repetir sempre: ",
"Autoplay: ": "Reprodução automática: ",
"Play next by default: ": "Sempre reproduzir próximo: ",
"Play next by default: ": "Reproduzir sempre o próximo: ",
"Autoplay next video: ": "Reproduzir próximo vídeo automaticamente: ",
"Listen by default: ": "Apenas áudio: ",
"Proxy videos: ": "Usar proxy nos vídeos: ",
@ -65,12 +71,14 @@
"Preferred video quality: ": "Qualidade de vídeo preferida: ",
"Player volume: ": "Volume da reprodução: ",
"Default comments: ": "Preferência dos comentários: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Legendas predefinidas: ",
"Fallback captions: ": "Legendas alternativas: ",
"Show related videos: ": "Mostrar vídeos relacionados: ",
"Show annotations by default: ": "Mostrar sempre anotações: ",
"Show annotations by default: ": "Mostrar anotações sempre: ",
"Automatically extend video description: ": "Estender automaticamente a descrição do vídeo: ",
"Interactive 360 degree videos: ": "Vídeos interativos de 360 graus: ",
"Visual preferences": "Preferências visuais",
"Player style: ": "Estilo do reprodutor: ",
"Dark mode: ": "Modo escuro: ",
@ -78,6 +86,8 @@
"dark": "escuro",
"light": "claro",
"Thin mode: ": "Modo compacto: ",
"Miscellaneous preferences": "Preferências diversas",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Redirecionamento de instância automática (solução de último recurso para redirect.invidious.io): ",
"Subscription preferences": "Preferências de subscrições",
"Show annotations by default for subscribed channels: ": "Mostrar sempre anotações aos canais subscritos: ",
"Redirect homepage to feed: ": "Redirecionar página inicial para subscrições: ",
@ -98,37 +108,44 @@
"`x` is live": "`x` está em direto",
"Data preferences": "Preferências de dados",
"Clear watch history": "Limpar histórico de reprodução",
"Import/export data": "Importar/Exportar dados",
"Import/export data": "Importar / exportar dados",
"Change password": "Alterar palavra-chave",
"Manage subscriptions": "Gerir as subscrições",
"Manage tokens": "Gerir tokens",
"Watch history": "Histórico de reprodução",
"Delete account": "Apagar conta",
"Delete account": "Eliminar conta",
"Administrator preferences": "Preferências de administrador",
"Default homepage: ": "Página inicial predefinida: ",
"Feed menu: ": "Menu de subscrições: ",
"Top enabled: ": "Top ativado: ",
"Show nickname on top: ": "Mostrar nome de utilizador em cima: ",
"Top enabled: ": "Destaques ativados: ",
"CAPTCHA enabled: ": "CAPTCHA ativado: ",
"Login enabled: ": "Iniciar sessão ativado: ",
"Registration enabled: ": "Registar ativado: ",
"Report statistics: ": "Relatório de estatísticas: ",
"Save preferences": "Gravar preferências",
"Save preferences": "Guardar preferências",
"Subscription manager": "Gerir subscrições",
"Token manager": "Gerir tokens",
"Token": "Token",
"`x` subscriptions..([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscrições.",
"`x` subscriptions..": "`x` subscrições.",
"`x` tokens..([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens.",
"`x` tokens..": "`x` tokens.",
"Import/export": "Importar/Exportar",
"unsubscribe": "Anular subscrição",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscrições",
"": "`x` subscrições"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens",
"": "`x` tokens"
},
"Import/export": "Importar / exportar",
"unsubscribe": "anular subscrição",
"revoke": "revogar",
"Subscriptions": "Subscrições",
"`x` unseen notifications..([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificações não vistas.",
"`x` unseen notifications..": "`x` notificações não vistas.",
"search": "Pesquisar",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificações não vistas",
"": "`x` notificações não vistas"
},
"search": "pesquisar",
"Log out": "Terminar sessão",
"Released under the AGPLv3 by Omar Roth.": "Publicado sob a licença AGPLv3, por Omar Roth.",
"Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no Github.",
"Source available here.": "Código-fonte disponível aqui.",
"View JavaScript license information.": "Ver informações da licença do JavaScript.",
"View privacy policy.": "Ver a política de privacidade.",
@ -138,13 +155,17 @@
"Private": "Privado",
"View all playlists": "Ver todas as listas de reprodução",
"Updated `x` ago": "Atualizado `x` atrás",
"Delete playlist `x`?": "Apagar a lista de reprodução 'x'?",
"Delete playlist": "Apagar lista de reprodução",
"Delete playlist `x`?": "Eliminar a lista de reprodução 'x'?",
"Delete playlist": "Eliminar lista de reprodução",
"Create playlist": "Criar lista de reprodução",
"Title": "Título",
"Playlist privacy": "Privacidade da lista de reprodução",
"Editing playlist `x`": "A editar lista de reprodução 'x'",
"Show more": "Mostrar mais",
"Show less": "Mostrar menos",
"Watch on YouTube": "Ver no YouTube",
"Switch Invidious Instance": "Mudar a instância do Invidious",
"Broken? Try another Invidious Instance": "Falhou? Tente outra Instância do Invidious",
"Hide annotations": "Ocultar anotações",
"Show annotations": "Mostrar anotações",
"Genre: ": "Género: ",
@ -155,23 +176,27 @@
"Whitelisted regions: ": "Regiões permitidas: ",
"Blacklisted regions: ": "Regiões bloqueadas: ",
"Shared `x`": "Partilhado `x`",
"`x` views..([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizações.",
"`x` views..": "`x` visualizações.",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizações",
"": "`x` visualizações"
},
"Premieres in `x`": "Estreias em 'x'",
"Premieres `x`": "Estreias '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.": "Oi! Parece que JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.",
"View YouTube comments": "Ver comentários do YouTube",
"View more comments on Reddit": "Ver mais comentários no Reddit",
"View `x` comments..([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários.",
"View `x` comments..": "Ver `x` comentários.",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários",
"": "Ver `x` comentários"
},
"View Reddit comments": "Ver comentários do Reddit",
"Hide replies": "Ocultar respostas",
"Show replies": "Mostrar respostas",
"Incorrect password": "Palavra-chave incorreta",
"Quota exceeded, try again in a few hours": "Cota excedida. Tente novamente dentro de algumas horas",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não é possível iniciar sessão, certifique-se de que a autenticação de dois fatores (Autenticador ou SMS) está ativada.",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não é possível iniciar a sessão, certifique-se que a autenticação de dois fatores (Autenticador ou SMS) está ativada.",
"Invalid TFA code": "Código TFA inválido",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Falhou o início de sessão. Isto pode ser devido a dois fatores de autenticação não está ativado para sua conta.",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Falhou o início de sessão. Isto pode ser devido a não ter ativado na sua conta a autenticação de dois fatores (2FA).",
"Wrong answer": "Resposta errada",
"Erroneous CAPTCHA": "CAPTCHA inválido",
"CAPTCHA is a required field": "CAPTCHA é um campo obrigatório",
@ -184,21 +209,25 @@
"Please log in": "Por favor, inicie sessão",
"Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`",
"channel:`x`": "canal:'x'",
"Deleted or invalid channel": "Canal apagado ou inválido",
"Deleted or invalid channel": "Canal eliminado ou inválido",
"This channel does not exist.": "Este canal não existe.",
"Could not get channel info.": "Não foi possível obter as informações do canal.",
"Could not fetch comments": "Não foi possível obter os comentários",
"View `x` replies..([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` respostas.",
"View `x` replies..": "Ver `x` respostas.",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` respostas",
"": "Ver `x` respostas"
},
"`x` ago": "`x` atrás",
"Load more": "Carregar mais",
"`x` points..([^.,0-9]|^)1([^.,0-9]|$)": "'x' pontos.",
"`x` points..": "'x' pontos.",
"Could not create mix.": "Não foi possível criar mistura.",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pontos",
"": "`x` pontos"
},
"Could not create mix.": "Não foi possível criar a mistura.",
"Empty playlist": "Lista de reprodução vazia",
"Not a playlist.": "Não é uma lista de reprodução.",
"Playlist does not exist.": "A lista de reprodução não existe.",
"Could not pull trending pages.": "Não foi possível obter páginas de tendências.",
"Could not pull trending pages.": "Não foi possível obter as páginas de tendências.",
"Hidden field \"challenge\" is a required field": "O campo oculto \"desafio\" é obrigatório",
"Hidden field \"token\" is a required field": "O campo oculto \"token\" é um campo obrigatório",
"Erroneous challenge": "Desafio inválido",
@ -221,8 +250,8 @@
"Burmese": "Birmanês",
"Catalan": "Catalão",
"Cebuano": "Cebuano",
"Chinese (Simplified)": "Chinês (Simplificado)",
"Chinese (Traditional)": "Chinês (Tradicional)",
"Chinese (Simplified)": "Chinês (simplificado)",
"Chinese (Traditional)": "Chinês (tradicional)",
"Corsican": "Corso",
"Croatian": "Croata",
"Czech": "Checo",
@ -311,43 +340,88 @@
"Yiddish": "Iídiche",
"Yoruba": "Ioruba",
"Zulu": "Zulu",
"`x` years..([^.,0-9]|^)1([^.,0-9]|$)": "`x` anos.",
"`x` years..": "`x` anos.",
"`x` months..([^.,0-9]|^)1([^.,0-9]|$)": "`x` meses.",
"`x` months..": "`x` meses.",
"`x` weeks..([^.,0-9]|^)1([^.,0-9]|$)": "`x` semanas.",
"`x` weeks..": "`x` semanas.",
"`x` days..([^.,0-9]|^)1([^.,0-9]|$)": "`x` dias.",
"`x` days..": "`x` dias.",
"`x` hours..([^.,0-9]|^)1([^.,0-9]|$)": "`x` horas.",
"`x` hours..": "`x` horas.",
"`x` minutes..([^.,0-9]|^)1([^.,0-9]|$)": "`x` minutos.",
"`x` minutes..": "`x` minutos.",
"`x` seconds..([^.,0-9]|^)1([^.,0-9]|$)": "`x` segundos.",
"`x` seconds..": "`x` segundos.",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ano",
"": "`x` anos"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` mês",
"": "`x` meses"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` seman",
"": "`x` semanas"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dia",
"": "`x` dias"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` hora",
"": "`x` horas"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto",
"": "`x` minutos"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` segundo",
"": "`x` segundos"
},
"Fallback comments: ": "Comentários alternativos: ",
"Popular": "Popular",
"Top": "Top",
"Search": "Pesquisar",
"Top": "Destaques",
"About": "Sobre",
"Rating: ": "Avaliação: ",
"Language: ": "Idioma: ",
"View as playlist": "Ver como lista de reprodução",
"Default": "Predefinição",
"Default": "Predefinido",
"Music": "Música",
"Gaming": "Jogos",
"News": "Notícias",
"Movies": "Filmes",
"Download": "Transferir",
"Download as: ": "Transferir como: ",
"Download": "Descarregar",
"Download as: ": "Descarregar como: ",
"%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "(editado)",
"YouTube comment permalink": "Hiperligação permanente ao comentário do YouTube",
"permalink": "ligação permanente",
"YouTube comment permalink": "Hiperligação permanente do comentário no YouTube",
"permalink": "hiperligação permanente",
"`x` marked it with a ❤": "`x` foi marcado como ❤",
"Audio mode": "Modo de áudio",
"Video mode": "Modo de vídeo",
"Videos": "Vídeos",
"Playlists": "Listas de reprodução",
"Community": "Comunidade",
"Current version: ": "Versão atual: "
"relevance": "Relevância",
"rating": "Avaliação",
"date": "Data de envio",
"views": "Visualizações",
"content_type": "Tipo",
"duration": "Duração",
"features": "Funcionalidades",
"sort": "Ordenar por",
"hour": "Última hora",
"today": "Hoje",
"week": "Esta semana",
"month": "Este mês",
"year": "Este ano",
"video": "Vídeo",
"channel": "Canal",
"playlist": "Lista de reprodução",
"movie": "Filme",
"show": "Espetáculo",
"hd": "HD",
"subtitles": "Legendas",
"creative_commons": "Creative Commons",
"3d": "3D",
"live": "Em direto",
"4k": "4K",
"location": "Localização",
"hdr": "HDR",
"filter": "Filtro",
"Current version: ": "Versão atual: ",
"next_steps_error_message": "Pode tentar as seguintes opções: ",
"next_steps_error_message_refresh": "Atualizar",
"next_steps_error_message_go_to_youtube": "Ir ao YouTube"
}

427
locales/pt.json Normal file
View File

@ -0,0 +1,427 @@
{
"show": "Espetáculo",
"views": "Visualizações",
"date": "Data de envio",
"rating": "Avaliação",
"relevance": "Relevância",
"Broken? Try another Invidious Instance": "Falhou? Tente outra Instância do Invidious",
"Switch Invidious Instance": "Mudar a instância do Invidious",
"Show less": "Mostrar menos",
"Show more": "Mostrar mais",
"Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no Github.",
"Show nickname on top: ": "Mostrar nome de utilizador em cima: ",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Redirecionamento de instância automática (solução de último recurso para redirect.invidious.io): ",
"Miscellaneous preferences": "Preferências diversas",
"Interactive 360 degree videos: ": "Vídeos interativos de 360 graus: ",
"Automatically extend video description: ": "Estender automaticamente a descrição do vídeo: ",
"next_steps_error_message_go_to_youtube": "Ir ao YouTube",
"next_steps_error_message": "Pode tentar as seguintes opções: ",
"next_steps_error_message_refresh": "Atualizar",
"filter": "Filtro",
"hdr": "HDR",
"location": "Localização",
"4k": "4K",
"live": "Em direto",
"3d": "3D",
"creative_commons": "Creative Commons",
"subtitles": "Legendas",
"hd": "HD",
"movie": "Filme",
"playlist": "Lista de reprodução",
"channel": "Canal",
"video": "Vídeo",
"year": "Este ano",
"month": "Este mês",
"week": "Esta semana",
"today": "Hoje",
"hour": "Última hora",
"sort": "Ordenar por",
"features": "Funcionalidades",
"duration": "Duração",
"content_type": "Tipo",
"permalink": "hiperligação permanente",
"YouTube comment permalink": "Hiperligação permanente do comentário no YouTube",
"Download as: ": "Descarregar como: ",
"Download": "Descarregar",
"Default": "Predefinido",
"Top": "Destaques",
"Search": "Pesquisar",
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` segundo",
"": "`x` segundos"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto",
"": "`x` minutos"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` hora",
"": "`x` horas"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dia",
"": "`x` dias"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` seman",
"": "`x` semanas"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` mês",
"": "`x` meses"
},
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ano",
"": "`x` anos"
},
"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.",
"Could not create mix.": "Não foi possível criar a mistura.",
"Deleted or invalid channel": "Canal eliminado ou inválido",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Falhou o início de sessão. Isto pode ser devido a não ter ativado na sua conta a autenticação de dois fatores (2FA).",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não é possível iniciar a sessão, certifique-se que a autenticação de dois fatores (Autenticador ou SMS) está ativada.",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.",
"Delete playlist": "Eliminar lista de reprodução",
"Delete playlist `x`?": "Eliminar a lista de reprodução 'x'?",
"search": "pesquisar",
"unsubscribe": "anular subscrição",
"Import/export": "Importar / exportar",
"Save preferences": "Guardar preferências",
"Top enabled: ": "Destaques ativados: ",
"Delete account": "Eliminar conta",
"Import/export data": "Importar / exportar dados",
"Show annotations by default: ": "Mostrar anotações sempre: ",
"Play next by default: ": "Reproduzir sempre o próximo: ",
"Sign In": "Iniciar sessão",
"Log in/register": "Iniciar sessão/registar",
"Delete account?": "Eliminar conta?",
"Import and Export Data": "Importar e exportar dados",
"Cannot change password for Google accounts": "Não é possível alterar a palavra-chave para contas do Google",
"Filipino": "Filipino",
"Estonian": "Estónio",
"Esperanto": "Esperanto",
"Dutch": "Holandês",
"Danish": "Dinamarquês",
"Czech": "Checo",
"Croatian": "Croata",
"Corsican": "Corso",
"Cebuano": "Cebuano",
"Catalan": "Catalão",
"Burmese": "Birmanês",
"Bulgarian": "Búlgaro",
"Bosnian": "Bósnio",
"Belarusian": "Bielorrusso",
"Basque": "Basco",
"Bangla": "Bangla",
"Azerbaijani": "Azerbaijano",
"Armenian": "Arménio",
"Arabic": "Árabe",
"Amharic": "Amárico",
"Albanian": "Albanês",
"Afrikaans": "Africano",
"English (auto-generated)": "Inglês (auto-gerado)",
"English": "Inglês",
"Token is expired, please try again": "Token expirou, tente novamente",
"No such user": "Utilizador inválido",
"Erroneous token": "Token inválido",
"Erroneous challenge": "Desafio inválido",
"Hidden field \"token\" is a required field": "O campo oculto \"token\" é um campo obrigatório",
"Hidden field \"challenge\" is a required field": "O campo oculto \"desafio\" é obrigatório",
"Playlist does not exist.": "A lista de reprodução não existe.",
"Not a playlist.": "Não é uma lista de reprodução.",
"Empty playlist": "Lista de reprodução vazia",
"`x` points": {
"": "`x` pontos",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` pontos"
},
"Load more": "Carregar mais",
"`x` ago": "`x` atrás",
"View `x` replies": {
"": "Ver `x` respostas",
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` respostas"
},
"Could not fetch comments": "Não foi possível obter os comentários",
"Could not get channel info.": "Não foi possível obter as informações do canal.",
"This channel does not exist.": "Este canal não existe.",
"channel:`x`": "canal:'x'",
"Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`",
"Please log in": "Por favor, inicie sessão",
"Password cannot be longer than 55 characters": "A palavra-chave não pode ser superior a 55 caracteres",
"Password cannot be empty": "A palavra-chave não pode estar vazia",
"Please sign in using 'Log in with Google'": "Por favor, inicie sessão usando 'Iniciar sessão com o Google'",
"Wrong username or password": "Nome de utilizador ou palavra-chave incorreto",
"Password is a required field": "Palavra-chave é um campo obrigatório",
"User ID is a required field": "O nome de utilizador é um campo obrigatório",
"CAPTCHA is a required field": "CAPTCHA é um campo obrigatório",
"Erroneous CAPTCHA": "CAPTCHA inválido",
"Wrong answer": "Resposta errada",
"Invalid TFA code": "Código TFA inválido",
"Quota exceeded, try again in a few hours": "Cota excedida. Tente novamente dentro de algumas horas",
"Incorrect password": "Palavra-chave incorreta",
"Show replies": "Mostrar respostas",
"Hide replies": "Ocultar respostas",
"View Reddit comments": "Ver comentários do Reddit",
"View `x` comments": {
"": "Ver `x` comentários",
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários"
},
"View more comments on Reddit": "Ver mais comentários no Reddit",
"View YouTube comments": "Ver comentários do YouTube",
"Premieres `x`": "Estreias 'x'",
"Premieres in `x`": "Estreias em 'x'",
"`x` views": {
"": "`x` visualizações",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizações"
},
"Shared `x`": "Partilhado `x`",
"Blacklisted regions: ": "Regiões bloqueadas: ",
"Whitelisted regions: ": "Regiões permitidas: ",
"Engagement: ": "Compromisso: ",
"Wilson score: ": "Pontuação de Wilson: ",
"Family friendly? ": "Filtrar conteúdo impróprio: ",
"License: ": "Licença: ",
"Genre: ": "Género: ",
"Show annotations": "Mostrar anotações",
"Hide annotations": "Ocultar anotações",
"Watch on YouTube": "Ver no YouTube",
"Editing playlist `x`": "A editar lista de reprodução 'x'",
"Playlist privacy": "Privacidade da lista de reprodução",
"Title": "Título",
"Create playlist": "Criar lista de reprodução",
"Updated `x` ago": "Atualizado `x` atrás",
"View all playlists": "Ver todas as listas de reprodução",
"Private": "Privado",
"Unlisted": "Não listado",
"Public": "Público",
"Trending": "Tendências",
"View privacy policy.": "Ver a política de privacidade.",
"View JavaScript license information.": "Ver informações da licença do JavaScript.",
"Source available here.": "Código-fonte disponível aqui.",
"Log out": "Terminar sessão",
"`x` unseen notifications": {
"": "`x` notificações não vistas",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificações não vistas"
},
"Subscriptions": "Subscrições",
"revoke": "revogar",
"`x` tokens": {
"": "`x` tokens",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens"
},
"`x` subscriptions": {
"": "`x` subscrições",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscrições"
},
"Token": "Token",
"Token manager": "Gerir tokens",
"Subscription manager": "Gerir subscrições",
"Report statistics: ": "Relatório de estatísticas: ",
"Registration enabled: ": "Registar ativado: ",
"Login enabled: ": "Iniciar sessão ativado: ",
"CAPTCHA enabled: ": "CAPTCHA ativado: ",
"Feed menu: ": "Menu de subscrições: ",
"Default homepage: ": "Página inicial predefinida: ",
"Administrator preferences": "Preferências de administrador",
"Watch history": "Histórico de reprodução",
"Manage tokens": "Gerir tokens",
"Manage subscriptions": "Gerir as subscrições",
"Change password": "Alterar palavra-chave",
"Clear watch history": "Limpar histórico de reprodução",
"Data preferences": "Preferências de dados",
"`x` is live": "`x` está em direto",
"`x` uploaded a video": "`x` publicou um novo vídeo",
"Enable web notifications": "Ativar notificações pela web",
"Only show notifications (if there are any): ": "Mostrar apenas notificações (se existirem): ",
"Only show unwatched: ": "Mostrar apenas vídeos não visualizados: ",
"Only show latest unwatched video from channel: ": "Mostrar apenas vídeos mais recentes não visualizados do canal: ",
"Only show latest video from channel: ": "Mostrar apenas o vídeo mais recente do canal: ",
"channel name - reverse": "nome do canal - inverso",
"channel name": "nome do canal",
"alphabetically - reverse": "alfabeticamente - inverso",
"alphabetically": "alfabeticamente",
"published - reverse": "publicado - inverso",
"published": "publicado",
"Sort videos by: ": "Ordenar vídeos por: ",
"Number of videos shown in feed: ": "Quantidade de vídeos nas subscrições: ",
"Redirect homepage to feed: ": "Redirecionar página inicial para subscrições: ",
"Show annotations by default for subscribed channels: ": "Mostrar sempre anotações aos canais subscritos: ",
"Subscription preferences": "Preferências de subscrições",
"Thin mode: ": "Modo compacto: ",
"light": "claro",
"dark": "escuro",
"Theme: ": "Tema: ",
"Dark mode: ": "Modo escuro: ",
"Player style: ": "Estilo do reprodutor: ",
"Visual preferences": "Preferências visuais",
"Show related videos: ": "Mostrar vídeos relacionados: ",
"Fallback captions: ": "Legendas alternativas: ",
"Default captions: ": "Legendas predefinidas: ",
"reddit": "reddit",
"youtube": "YouTube",
"Default comments: ": "Preferência dos comentários: ",
"Player volume: ": "Volume da reprodução: ",
"Preferred video quality: ": "Qualidade de vídeo preferida: ",
"Default speed: ": "Velocidade preferida: ",
"Proxy videos: ": "Usar proxy nos vídeos: ",
"Listen by default: ": "Apenas áudio: ",
"Autoplay next video: ": "Reproduzir próximo vídeo automaticamente: ",
"Autoplay: ": "Reprodução automática: ",
"Always loop: ": "Repetir sempre: ",
"Player preferences": "Preferências do reprodutor",
"Preferences": "Preferências",
"Google verification code": "Código de verificação do Google",
"E-mail": "E-mail",
"Register": "Registar",
"Image CAPTCHA": "Imagem CAPTCHA",
"Text CAPTCHA": "Texto CAPTCHA",
"Time (h:mm:ss):": "Tempo (h:mm:ss):",
"Password": "Palavra-chave",
"User ID": "Utilizador",
"Log in with Google": "Iniciar sessão com o Google",
"Log in": "Iniciar sessão",
"source": "código-fonte",
"JavaScript license information": "Informação de licença do JavaScript",
"An alternative front-end to YouTube": "Uma interface alternativa ao YouTube",
"History": "Histórico",
"Export data as JSON": "Exportar dados como JSON",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar subscrições como OPML (para NewPipe e FreeTube)",
"Export subscriptions as OPML": "Exportar subscrições como OPML",
"Export": "Exportar",
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
"Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
"Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)",
"Import YouTube subscriptions": "Importar subscrições do YouTube",
"Import Invidious data": "Importar dados do Invidious",
"Import": "Importar",
"No": "Não",
"Yes": "Sim",
"Authorize token for `x`?": "Autorizar token para `x`?",
"Authorize token?": "Autorizar token?",
"New passwords must match": "As novas palavra-chaves devem corresponder",
"New password": "Nova palavra-chave",
"Clear watch history?": "Limpar histórico de reprodução?",
"Previous page": "Página anterior",
"Next page": "Próxima página",
"last": "últimos",
"Current version: ": "Versão atual: ",
"Community": "Comunidade",
"Playlists": "Listas de reprodução",
"Videos": "Vídeos",
"Video mode": "Modo de vídeo",
"Audio mode": "Modo de áudio",
"`x` marked it with a ❤": "`x` foi marcado como ❤",
"(edited)": "(editado)",
"%A %B %-d, %Y": "%A %B %-d, %Y",
"Movies": "Filmes",
"News": "Notícias",
"Gaming": "Jogos",
"Music": "Música",
"View as playlist": "Ver como lista de reprodução",
"Language: ": "Idioma: ",
"Rating: ": "Avaliação: ",
"About": "Sobre",
"Popular": "Popular",
"Fallback comments: ": "Comentários alternativos: ",
"Zulu": "Zulu",
"Yoruba": "Ioruba",
"Yiddish": "Iídiche",
"Xhosa": "Xhosa",
"Western Frisian": "Frísio Ocidental",
"Welsh": "Galês",
"Vietnamese": "Vietnamita",
"Uzbek": "Uzbeque",
"Urdu": "Urdu",
"Ukrainian": "Ucraniano",
"Turkish": "Turco",
"Thai": "Tailandês",
"Telugu": "Telugu",
"Tamil": "Tâmil",
"Tajik": "Tajique",
"Swedish": "Sueco",
"Swahili": "Suaíli",
"Sundanese": "Sudanês",
"Spanish (Latin America)": "Espanhol (América Latina)",
"Spanish": "Espanhol",
"Southern Sotho": "Sotho do Sul",
"Somali": "Somali",
"Slovenian": "Esloveno",
"Slovak": "Eslovaco",
"Sinhala": "Cingalês",
"Sindhi": "Sindhi",
"Shona": "Shona",
"Serbian": "Sérvio",
"Scottish Gaelic": "Gaélico escocês",
"Samoan": "Samoano",
"Russian": "Russo",
"Romanian": "Romeno",
"Punjabi": "Punjabi",
"Portuguese": "Português",
"Polish": "Polaco",
"Persian": "Persa",
"Pashto": "Pashto",
"Nyanja": "Nyanja",
"Norwegian Bokmål": "Bokmål norueguês",
"Nepali": "Nepalês",
"Mongolian": "Mongol",
"Marathi": "Marathi",
"Maori": "Maori",
"Maltese": "Maltês",
"Malayalam": "Malaiala",
"Malay": "Malaio",
"Malagasy": "Malgaxe",
"Macedonian": "Macedónio",
"Luxembourgish": "Luxemburguês",
"Lithuanian": "Lituano",
"Latvian": "Letão",
"Latin": "Latim",
"Lao": "Laosiano",
"Kyrgyz": "Quirguiz",
"Kurdish": "Curdo",
"Korean": "Coreano",
"Khmer": "Khmer",
"Kazakh": "Cazaque",
"Kannada": "Canarim",
"Javanese": "Javanês",
"Japanese": "Japonês",
"Italian": "Italiano",
"Irish": "Irlandês",
"Indonesian": "Indonésio",
"Igbo": "Igbo",
"Icelandic": "Islandês",
"Hungarian": "Húngaro",
"Hmong": "Hmong",
"Hindi": "Hindi",
"Hebrew": "Hebraico",
"Hawaiian": "Havaiano",
"Hausa": "Hauçá",
"Haitian Creole": "Crioulo haitiano",
"Gujarati": "Guzerate",
"Greek": "Grego",
"German": "Alemão",
"Georgian": "Georgiano",
"Galician": "Galego",
"French": "Francês",
"Finnish": "Finlandês",
"popular": "popular",
"oldest": "mais antigos",
"newest": "mais recentes",
"View playlist on YouTube": "Ver lista de reprodução no YouTube",
"View channel on YouTube": "Ver canal no YouTube",
"Subscribe": "Subscrever",
"Unsubscribe": "Anular subscrição",
"Shared `x` ago": "Partilhado `x` atrás",
"LIVE": "Em direto",
"`x` playlists": {
"": "`x` listas de reprodução",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` listas de reprodução"
},
"`x` videos": {
"": "`x` vídeos",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videos"
},
"`x` subscribers": {
"": "`x` subscritores",
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscritores"
}
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` abonați",
"`x` videos": "`x` videoclipuri",
"`x` playlists": "`x` liste de redare",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonați",
"": "`x` abonați"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videoclipuri",
"": "`x` videoclipuri"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` liste de redare",
"": "`x` liste de redare"
},
"LIVE": "ÎN DIRECT",
"Shared `x` ago": "Adăugat acum `x`",
"Unsubscribe": "Dezabonați-vă",
@ -68,6 +77,8 @@
"Fallback captions: ": "Subtitrări alternative: ",
"Show related videos: ": "Afișați videoclipurile asemănătoare: ",
"Show annotations by default: ": "Afișați adnotările în mod implicit: ",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "Preferințele site-ului",
"Player style: ": "Stilul player-ului : ",
"Dark mode: ": "Modul întunecat : ",
@ -75,6 +86,8 @@
"dark": "întunecat",
"light": "luminos",
"Thin mode: ": "Mod lejer: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Preferințele paginii de abonamente",
"Show annotations by default for subscribed channels: ": "Afișați adnotările în mod implicit pentru canalele la care v-ați abonat: ",
"Redirect homepage to feed: ": "Redirecționați pagina principală la pagina de abonamente: ",
@ -104,6 +117,7 @@
"Administrator preferences": "Preferințele Administratorului",
"Default homepage: ": "Pagina principală implicită: ",
"Feed menu: ": "Preferințe legate de pagina de abonamente: ",
"Show nickname on top: ": "",
"Top enabled: ": "Top activat: ",
"CAPTCHA enabled: ": "CAPTCHA activat : ",
"Login enabled: ": "Autentificare activată : ",
@ -113,16 +127,25 @@
"Subscription manager": "Gestionați abonamentele",
"Token manager": "Manager de Tokene",
"Token": "Token",
"`x` subscriptions": "`x` abonamente",
"`x` tokens": "`x` tokens",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonamente",
"": "`x` abonamente"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens",
"": "`x` tokens"
},
"Import/export": "Importați/Exportați",
"unsubscribe": "dezabonați-vă",
"revoke": "revocați",
"Subscriptions": "Abonamente",
"`x` unseen notifications": "`x` notificări nevăzute",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificări nevăzute",
"": "`x` notificări nevăzute"
},
"search": "căutați",
"Log out": "Deconectați-vă",
"Released under the AGPLv3 by Omar Roth.": "Publicat sub licența AGPLv3 de Omar Roth.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Codul sursă este disponibil aici.",
"View JavaScript license information.": "Informații legate de licența JavaScript.",
"View privacy policy.": "Politica de confidențialitate.",
@ -138,7 +161,11 @@
"Title": "Titlu",
"Playlist privacy": "Parametrii de confidențialitate ai listei de redare",
"Editing playlist `x`": "Modificați lista de redare `x`",
"Show more": "",
"Show less": "",
"Watch on YouTube": "Urmăriți videoclipul pe YouTube",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "Ascundeți adnotările",
"Show annotations": "Afișați adnotările",
"Genre: ": "Categorie: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Regiunile de pe lista albă: ",
"Blacklisted regions: ": "Regiunile de pe lista neagră: ",
"Shared `x`": "Publicat pe `x`",
"`x` views": "`x` vizionări",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` vizionări",
"": "`x` vizionări"
},
"Premieres in `x`": "Premiera în `x`",
"Premieres `x`": "Premiera pe `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.": "Se pare că ați dezactivat JavaScript. Apăsați aici pentru a vizualiza comentariile. Țineți minte faptul că încărcarea lor ar putea să dureze puțin mai mult.",
"View YouTube comments": "Vedeți comentariile de pe YouTube",
"View more comments on Reddit": "Vedeți mai multe comentarii pe Reddit",
"View `x` comments": "Afișați `x` comentarii",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Afișați `x` comentarii",
"": "Afișați `x` comentarii"
},
"View Reddit comments": "Afișați comentariile de pe Reddit",
"Hide replies": "Ascundeți replicile",
"Show replies": "Afișați replicile",
@ -180,10 +213,16 @@
"This channel does not exist.": "Acest canal nu există.",
"Could not get channel info.": "Nu am putut primi informații despre acest canal.",
"Could not fetch comments": "Încărcarea comentariilor a eșuat.",
"View `x` replies": "Afișați `x` replici",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Afișați `x` replici",
"": "Afișați `x` replici"
},
"`x` ago": "acum `x`",
"Load more": "Vedeți mai mult",
"`x` points": "`x` puncte",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` puncte",
"": "`x` puncte"
},
"Could not create mix.": "Nu am putut crea această listă de redare.",
"Empty playlist": "Lista de redare este goală",
"Not a playlist.": "Lista de redare este invalidă.",
@ -301,15 +340,37 @@
"Yiddish": "Yiddish",
"Yoruba": "Yoruba",
"Zulu": "Zoulou",
"`x` years": "`x` ani",
"`x` months": "`x` luni",
"`x` weeks": "`x` săptămâni",
"`x` days": "`x` zile",
"`x` hours": "`x` ore",
"`x` minutes": "`x` minute",
"`x` seconds": "`x` secunde",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ani",
"": "`x` ani"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` luni",
"": "`x` luni"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` săptămâni",
"": "`x` săptămâni"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` zile",
"": "`x` zile"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ore",
"": "`x` ore"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minute",
"": "`x` minute"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` secunde",
"": "`x` secunde"
},
"Fallback comments: ": "Comentarii alternative: ",
"Popular": "Popular",
"Search": "",
"Top": "Top",
"About": "Despre",
"Rating: ": "Evaluare: ",
@ -332,5 +393,35 @@
"Videos": "Videoclipuri",
"Playlists": "Liste de redare",
"Community": "Comunitate",
"Current version: ": "Versiunea actuală: "
}
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "Versiunea actuală: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` подписчиков",
"`x` videos": "`x` видео",
"`x` playlists": "`x` плейлистов",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` подписчиков",
"": "`x` подписчиков"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` видео",
"": "`x` видео"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` плейлистов",
"": "`x` плейлистов"
},
"LIVE": "ПРЯМОЙ ЭФИР",
"Shared `x` ago": "Опубликовано `x` назад",
"Unsubscribe": "Отписаться",
@ -68,6 +77,8 @@
"Fallback captions: ": "Дополнительный язык субтитров: ",
"Show related videos: ": "Показывать похожие видео? ",
"Show annotations by default: ": "Всегда показывать аннотации? ",
"Automatically extend video description: ": "Автоматически раскрывать описание видео: ",
"Interactive 360 degree videos: ": "Интерактивные 360-градусные видео: ",
"Visual preferences": "Настройки сайта",
"Player style: ": "Стиль проигрывателя: ",
"Dark mode: ": "Тёмное оформление: ",
@ -75,6 +86,8 @@
"dark": "темная",
"light": "светлая",
"Thin mode: ": "Облегчённое оформление: ",
"Miscellaneous preferences": "Прочие предпочтения",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Автоматическое перенаправление экземпляра (резервный вариант redirect.invidious.io): ",
"Subscription preferences": "Настройки подписок",
"Show annotations by default for subscribed channels: ": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ",
"Redirect homepage to feed: ": "Отображать видео с каналов, на которые вы подписаны, как главную страницу: ",
@ -104,6 +117,7 @@
"Administrator preferences": "Администраторские настройки",
"Default homepage: ": "Главная страница по умолчанию: ",
"Feed menu: ": "Меню ленты видео: ",
"Show nickname on top: ": "Показать ник вверху: ",
"Top enabled: ": "Включить топ видео? ",
"CAPTCHA enabled: ": "Включить капчу? ",
"Login enabled: ": "Включить авторизацию? ",
@ -113,16 +127,25 @@
"Subscription manager": "Менеджер подписок",
"Token manager": "Менеджер токенов",
"Token": "Токен",
"`x` subscriptions": "`x` подписок",
"`x` tokens": "`x` токенов",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` подписок",
"": "`x` подписок"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` токенов",
"": "`x` токенов"
},
"Import/export": "Импорт и экспорт",
"unsubscribe": "отписаться",
"revoke": "отозвать",
"Subscriptions": "Подписки",
"`x` unseen notifications": "`x` непросмотренных оповещений",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` непросмотренных оповещений",
"": "`x` непросмотренных оповещений"
},
"search": "поиск",
"Log out": "Выйти",
"Released under the AGPLv3 by Omar Roth.": "Реализовано Омаром Ротом по лицензии AGPLv3.",
"Released under the AGPLv3 on Github.": "Выпущено под лицензией AGPLv3 на Github.",
"Source available here.": "Исходный код доступен здесь.",
"View JavaScript license information.": "Посмотреть информацию по лицензии JavaScript.",
"View privacy policy.": "Посмотреть политику конфиденциальности.",
@ -138,7 +161,11 @@
"Title": "Заголовок",
"Playlist privacy": "Конфиденциальность плейлиста",
"Editing playlist `x`": "Редактирование плейлиста `x`",
"Show more": "Показать больше",
"Show less": "Показать меньше",
"Watch on YouTube": "Смотреть на YouTube",
"Switch Invidious Instance": "Сменить экземпляр Invidious",
"Broken? Try another Invidious Instance": "Сломался? Попробуйте другой экземпляр Invidious",
"Hide annotations": "Скрыть аннотации",
"Show annotations": "Показать аннотации",
"Genre: ": "Жанр: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Доступно в регионах: ",
"Blacklisted regions: ": "Недоступно в регионах: ",
"Shared `x`": "Опубликовано `x`",
"`x` views": "`x` просмотров",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` просмотров",
"": "`x` просмотров"
},
"Premieres in `x`": "Премьера через `x`",
"Premieres `x`": "Премьера `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.": "Похоже, у вас отключён JavaScript. Чтобы увидить комментарии, нажмите сюда, но учтите: они могут загружаться немного медленнее.",
"View YouTube comments": "Смотреть комментарии с YouTube",
"View more comments on Reddit": "Посмотреть больше комментариев на Reddit",
"View `x` comments": "Показать `x` комментариев",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Показать `x` комментариев",
"": "Показать `x` комментариев"
},
"View Reddit comments": "Смотреть комментарии с Reddit",
"Hide replies": "Скрыть ответы",
"Show replies": "Показать ответы",
@ -180,10 +213,16 @@
"This channel does not exist.": "Такого канала не существует.",
"Could not get channel info.": "Не удаётся получить информацию об этом канале.",
"Could not fetch comments": "Не удаётся загрузить комментарии",
"View `x` replies": "Показать `x` ответов",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Показать `x` ответов",
"": "Показать `x` ответов"
},
"`x` ago": "`x` назад",
"Load more": "Загрузить больше",
"`x` points": "`x` очков",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` очков",
"": "`x` очков"
},
"Could not create mix.": "Не удаётся создать микс.",
"Empty playlist": "Плейлист пуст",
"Not a playlist.": "Некорректный плейлист.",
@ -301,15 +340,37 @@
"Yiddish": "Идиш",
"Yoruba": "Йоруба",
"Zulu": "Зулусский",
"`x` years": "`x` лет",
"`x` months": "`x` месяцев",
"`x` weeks": "`x` недель",
"`x` days": "`x` дней",
"`x` hours": "`x` часов",
"`x` minutes": "`x` минут",
"`x` seconds": "`x` секунд",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` лет",
"": "`x` лет"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` месяцев",
"": "`x` месяцев"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` недель",
"": "`x` недель"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` дней",
"": "`x` дней"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` часов",
"": "`x` часов"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` минут",
"": "`x` минут"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` секунд",
"": "`x` секунд"
},
"Fallback comments: ": "Резервные комментарии: ",
"Popular": "Популярное",
"Search": "Поиск",
"Top": "Топ",
"About": "О сайте",
"Rating: ": "Рейтинг: ",
@ -332,5 +393,35 @@
"Videos": "Видео",
"Playlists": "Плейлисты",
"Community": "Сообщество",
"Current version: ": "Текущая версия: "
}
"relevance": "Актуальность",
"rating": "Рейтинг",
"date": "Дата загрузки",
"views": "Просмотры",
"content_type": "Тип",
"duration": "Длительность",
"features": "Функции",
"sort": "Сортировать по",
"hour": "Последний час",
"today": "Сегодня",
"week": "Эта неделя",
"month": "Этот месяц",
"year": "Этот год",
"video": "Видео",
"channel": "Канал",
"playlist": "Плейлист",
"movie": "Фильм",
"show": "Показать",
"hd": "HD",
"subtitles": "Субтитры",
"creative_commons": "Creative Commons",
"3d": "3D",
"live": "Прямой эфир",
"4k": "4K",
"location": "Местоположение",
"hdr": "HDR",
"filter": "Фильтр",
"Current version: ": "Текущая версия: ",
"next_steps_error_message": "После чего следует попробовать: ",
"next_steps_error_message_refresh": "Обновить",
"next_steps_error_message_go_to_youtube": "Перейти на YouTube"
}

View File

@ -77,6 +77,8 @@
"Fallback captions: ": "",
"Show related videos: ": "",
"Show annotations by default: ": "",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "",
"Player style: ": "",
"Dark mode: ": "",
@ -84,6 +86,8 @@
"dark": "",
"light": "",
"Thin mode: ": "",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "",
"Show annotations by default for subscribed channels: ": "",
"Redirect homepage to feed: ": "",
@ -113,6 +117,7 @@
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Show nickname on top: ": "",
"Top enabled: ": "",
"CAPTCHA enabled: ": "",
"Login enabled: ": "",
@ -140,7 +145,7 @@
},
"search": "",
"Log out": "",
"Released under the AGPLv3 by Omar Roth.": "",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "",
"View JavaScript license information.": "",
"View privacy policy.": "",
@ -156,7 +161,11 @@
"Title": "",
"Playlist privacy": "",
"Editing playlist `x`": "",
"Show more": "",
"Show less": "",
"Watch on YouTube": "",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "",
"Show annotations": "",
"Genre: ": "",
@ -361,6 +370,7 @@
},
"Fallback comments: ": "",
"Popular": "",
"Search": "",
"Top": "",
"About": "",
"Rating: ": "",
@ -410,5 +420,8 @@
"location": "",
"hdr": "",
"filter": "",
"Current version: ": ""
"Current version: ": "",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,10 +1,16 @@
{
"`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` subscribers.": "`x` odberateľov.",
"`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` videos.": "",
"`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` playlists.": "",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` odberateľov"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"LIVE": "NAŽIVO",
"Shared `x` ago": "",
"Unsubscribe": "Zrušiť odber",
@ -71,6 +77,8 @@
"Fallback captions: ": "Náhradné titulky: ",
"Show related videos: ": "Zobraziť súvisiace videá: ",
"Show annotations by default: ": "Predvolene zobraziť anotácie: ",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "Vizuálne nastavenia",
"Player style: ": "Štýl prehrávača: ",
"Dark mode: ": "Tmavý režim: ",
@ -78,6 +86,8 @@
"dark": "tmavá",
"light": "svetlá",
"Thin mode: ": "Tenký režim: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Nastavenia predplatného",
"Show annotations by default for subscribed channels: ": "Predvolene zobraziť anotácie odoberaných kanálov: ",
"Redirect homepage to feed: ": "Presmerovanie domovskej stránky na informačný kanál: ",
@ -107,6 +117,7 @@
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Show nickname on top: ": "",
"Top enabled: ": "",
"CAPTCHA enabled: ": "",
"Login enabled: ": "",
@ -116,19 +127,25 @@
"Subscription manager": "",
"Token manager": "",
"Token": "",
"`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` subscriptions.": "",
"`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` tokens.": "",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Import/export": "",
"unsubscribe": "",
"revoke": "",
"Subscriptions": "",
"`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` unseen notifications.": "",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"search": "",
"Log out": "",
"Released under the AGPLv3 by Omar Roth.": "",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "",
"View JavaScript license information.": "",
"View privacy policy.": "",
@ -144,7 +161,11 @@
"Title": "",
"Playlist privacy": "",
"Editing playlist `x`": "",
"Show more": "",
"Show less": "",
"Watch on YouTube": "",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "",
"Show annotations": "",
"Genre: ": "",
@ -155,15 +176,19 @@
"Whitelisted regions: ": "",
"Blacklisted regions: ": "",
"Shared `x`": "",
"`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` views.": "",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Premieres in `x`": "",
"Premieres `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.": "",
"View YouTube comments": "",
"View more comments on Reddit": "",
"View `x` comments.([^.,0-9]|^)1([^.,0-9]|$)": "",
"View `x` comments.": "",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"View Reddit comments": "",
"Hide replies": "",
"Show replies": "",
@ -188,12 +213,16 @@
"This channel does not exist.": "",
"Could not get channel info.": "",
"Could not fetch comments": "",
"View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "",
"View `x` replies.": "",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` ago": "",
"Load more": "",
"`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` points.": "",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Could not create mix.": "",
"Empty playlist": "",
"Not a playlist.": "",
@ -311,22 +340,37 @@
"Yiddish": "",
"Yoruba": "",
"Zulu": "",
"`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` years.": "",
"`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` months.": "",
"`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` weeks.": "",
"`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` days.": "",
"`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` hours.": "",
"`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` minutes.": "",
"`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "",
"`x` seconds.": "",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Fallback comments: ": "",
"Popular": "",
"Search": "",
"Top": "",
"About": "",
"Rating: ": "",
@ -349,5 +393,35 @@
"Videos": "",
"Playlists": "",
"Community": "",
"Current version: ": ""
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

427
locales/sr.json Normal file
View File

@ -0,0 +1,427 @@
{
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` пратилаца",
"": "`x` пратилаца"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` видео записа",
"": "`x` видео записа"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` списака извођења",
"": "`x` списака извођења"
},
"LIVE": "УЖИВО",
"Shared `x` ago": "Подељено пре `x`",
"Unsubscribe": "Прекини праћење",
"Subscribe": "Прати",
"View channel on YouTube": "Погледај канал на YouTube-у",
"View playlist on YouTube": "Погледај списак извођења на YouTube-у",
"newest": "најновије",
"oldest": "најстарије",
"popular": "гласовито",
"last": "последње",
"Next page": "Следећа страница",
"Previous page": "Претходна страница",
"Clear watch history?": "Избрисати повест прегледања?",
"New password": "Нова запорка",
"New passwords must match": "Нове запорке морају бити истоветне",
"Cannot change password for Google accounts": "Није могуће променити запорку за Google налоге",
"Authorize token?": "Овласти токен?",
"Authorize token for `x`?": "Овласти токен за `x`?",
"Yes": "Да",
"No": "Не",
"Import and Export Data": "Увоз и извоз података",
"Import": "Увези",
"Import Invidious data": "Увези податке са Invidious-а",
"Import YouTube subscriptions": "Увези праћења са YouTube-а",
"Import FreeTube subscriptions (.db)": "Увези праћења са FreeTube-а (.db)",
"Import NewPipe subscriptions (.json)": "Увези праћења са NewPipe-а (.json)",
"Import NewPipe data (.zip)": "Увези податке са NewPipe-а (.zip)",
"Export": "Извези",
"Export subscriptions as OPML": "Извези праћења као OPML датотеку",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Извези праћења као OPML датотеку (за NewPipe и FreeTube)",
"Export data as JSON": "Извези податке као JSON датотеку",
"Delete account?": "Избрисати рачун?",
"History": "Повест",
"An alternative front-end to YouTube": "Заменски кориснички слој за YouTube",
"JavaScript license information": "Извештај о JavaScript одобрењу",
"source": "извор",
"Log in": "Пријави се",
"Log in/register": "Пријави се/Отвори налог",
"Log in with Google": "Пријави се помоћу Google-а",
"User ID": "Кориснички ИД",
"Password": "Запорка",
"Time (h:mm:ss):": "Време (ч:мм:сс):",
"Text CAPTCHA": "Знаковни CAPTCHA",
"Image CAPTCHA": "Сликовни CAPTCHA",
"Sign In": "Пријава",
"Register": "Отвори налог",
"E-mail": "Е-пошта",
"Google verification code": "Google-ов оверни кôд",
"Preferences": "Подешавања",
"Player preferences": "Подешавања репродуктора",
"Always loop: ": "Увек понављај: ",
"Autoplay: ": "Самопуштање: ",
"Play next by default: ": "Увек подразумевано пуштај следеће: ",
"Autoplay next video: ": "Самопуштање следећег видео записа: ",
"Listen by default: ": "Увек подразумевано укључен само звук: ",
"Proxy videos: ": "Приказ видео записа преко посредника: ",
"Default speed: ": "",
"Preferred video quality: ": "",
"Player volume: ": "",
"Default comments: ": "",
"youtube": "",
"reddit": "",
"Default captions: ": "",
"Fallback captions: ": "",
"Show related videos: ": "",
"Show annotations by default: ": "",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "",
"Player style: ": "",
"Dark mode: ": "",
"Theme: ": "",
"dark": "",
"light": "",
"Thin mode: ": "",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "",
"Show annotations by default for subscribed channels: ": "",
"Redirect homepage to feed: ": "",
"Number of videos shown in feed: ": "",
"Sort videos by: ": "",
"published": "",
"published - reverse": "",
"alphabetically": "",
"alphabetically - reverse": "",
"channel name": "",
"channel name - reverse": "",
"Only show latest video from channel: ": "",
"Only show latest unwatched video from channel: ": "",
"Only show unwatched: ": "",
"Only show notifications (if there are any): ": "",
"Enable web notifications": "",
"`x` uploaded a video": "",
"`x` is live": "",
"Data preferences": "",
"Clear watch history": "",
"Import/export data": "",
"Change password": "",
"Manage subscriptions": "",
"Manage tokens": "",
"Watch history": "",
"Delete account": "",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Show nickname on top: ": "",
"Top enabled: ": "",
"CAPTCHA enabled: ": "",
"Login enabled: ": "",
"Registration enabled: ": "",
"Report statistics: ": "",
"Save preferences": "",
"Subscription manager": "",
"Token manager": "",
"Token": "",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Import/export": "",
"unsubscribe": "",
"revoke": "",
"Subscriptions": "",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"search": "",
"Log out": "",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "",
"View JavaScript license information.": "",
"View privacy policy.": "",
"Trending": "",
"Public": "",
"Unlisted": "",
"Private": "",
"View all playlists": "",
"Updated `x` ago": "",
"Delete playlist `x`?": "",
"Delete playlist": "",
"Create playlist": "",
"Title": "",
"Playlist privacy": "",
"Editing playlist `x`": "",
"Show more": "",
"Show less": "",
"Watch on YouTube": "",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "",
"Show annotations": "",
"Genre: ": "",
"License: ": "",
"Family friendly? ": "",
"Wilson score: ": "",
"Engagement: ": "",
"Whitelisted regions: ": "",
"Blacklisted regions: ": "",
"Shared `x`": "",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Premieres in `x`": "",
"Premieres `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.": "",
"View YouTube comments": "",
"View more comments on Reddit": "",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"View Reddit comments": "",
"Hide replies": "",
"Show replies": "",
"Incorrect password": "",
"Quota exceeded, try again in a few hours": "",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "",
"Invalid TFA code": "",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "",
"Wrong answer": "",
"Erroneous CAPTCHA": "",
"CAPTCHA is a required field": "",
"User ID is a required field": "",
"Password is a required field": "",
"Wrong username or password": "",
"Please sign in using 'Log in with Google'": "",
"Password cannot be empty": "",
"Password cannot be longer than 55 characters": "",
"Please log in": "",
"Invidious Private Feed for `x`": "",
"channel:`x`": "",
"Deleted or invalid channel": "",
"This channel does not exist.": "",
"Could not get channel info.": "",
"Could not fetch comments": "",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` ago": "",
"Load more": "",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Could not create mix.": "",
"Empty playlist": "",
"Not a playlist.": "",
"Playlist does not exist.": "",
"Could not pull trending pages.": "",
"Hidden field \"challenge\" is a required field": "",
"Hidden field \"token\" is a required field": "",
"Erroneous challenge": "",
"Erroneous token": "",
"No such user": "",
"Token is expired, please try again": "",
"English": "",
"English (auto-generated)": "",
"Afrikaans": "",
"Albanian": "",
"Amharic": "",
"Arabic": "",
"Armenian": "",
"Azerbaijani": "",
"Bangla": "",
"Basque": "",
"Belarusian": "",
"Bosnian": "",
"Bulgarian": "",
"Burmese": "",
"Catalan": "",
"Cebuano": "",
"Chinese (Simplified)": "",
"Chinese (Traditional)": "",
"Corsican": "",
"Croatian": "",
"Czech": "",
"Danish": "",
"Dutch": "",
"Esperanto": "",
"Estonian": "",
"Filipino": "",
"Finnish": "",
"French": "",
"Galician": "",
"Georgian": "",
"German": "",
"Greek": "",
"Gujarati": "",
"Haitian Creole": "",
"Hausa": "",
"Hawaiian": "",
"Hebrew": "",
"Hindi": "",
"Hmong": "",
"Hungarian": "",
"Icelandic": "",
"Igbo": "",
"Indonesian": "",
"Irish": "",
"Italian": "",
"Japanese": "",
"Javanese": "",
"Kannada": "",
"Kazakh": "",
"Khmer": "",
"Korean": "",
"Kurdish": "",
"Kyrgyz": "",
"Lao": "",
"Latin": "",
"Latvian": "",
"Lithuanian": "",
"Luxembourgish": "",
"Macedonian": "",
"Malagasy": "",
"Malay": "",
"Malayalam": "",
"Maltese": "",
"Maori": "",
"Marathi": "",
"Mongolian": "",
"Nepali": "",
"Norwegian Bokmål": "",
"Nyanja": "",
"Pashto": "",
"Persian": "",
"Polish": "",
"Portuguese": "",
"Punjabi": "",
"Romanian": "",
"Russian": "",
"Samoan": "",
"Scottish Gaelic": "",
"Serbian": "",
"Shona": "",
"Sindhi": "",
"Sinhala": "",
"Slovak": "",
"Slovenian": "",
"Somali": "",
"Southern Sotho": "",
"Spanish": "",
"Spanish (Latin America)": "",
"Sundanese": "",
"Swahili": "",
"Swedish": "",
"Tajik": "",
"Tamil": "",
"Telugu": "",
"Thai": "",
"Turkish": "",
"Ukrainian": "",
"Urdu": "",
"Uzbek": "",
"Vietnamese": "",
"Welsh": "",
"Western Frisian": "",
"Xhosa": "",
"Yiddish": "",
"Yoruba": "",
"Zulu": "",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Fallback comments: ": "",
"Popular": "",
"Search": "",
"Top": "",
"About": "",
"Rating: ": "",
"Language: ": "",
"View as playlist": "",
"Default": "",
"Music": "",
"Gaming": "",
"News": "",
"Movies": "",
"Download": "",
"Download as: ": "",
"%A %B %-d, %Y": "",
"(edited)": "",
"YouTube comment permalink": "",
"permalink": "",
"`x` marked it with a ❤": "",
"Audio mode": "",
"Video mode": "",
"Videos": "",
"Playlists": "",
"Community": "",
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers.": "%(count)s пратилац.",
"`x` videos.": "`x` видеа.",
"`x` playlists.": "`x` плејлиста/е.",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` пратилац"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` видеа"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` плејлиста/е"
},
"LIVE": "УЖИВО",
"Shared `x` ago": "Објављено пре `x`",
"Unsubscribe": "Прекините праћење",
@ -68,6 +77,8 @@
"Fallback captions: ": "Алтернативни титлови: ",
"Show related videos: ": "Прикажи сличне видее: ",
"Show annotations by default: ": "Увек приказуј анотације: ",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "Подешавања изгледа",
"Player style: ": "Стил плејера: ",
"Dark mode: ": "Тамни режим: ",
@ -75,6 +86,8 @@
"dark": "тамна",
"light": "светла",
"Thin mode: ": "Узани режим: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Подешавања о праћењима",
"Show annotations by default for subscribed channels: ": "Увек приказуј анотације за канале које пратим: ",
"Redirect homepage to feed: ": "Прикажи праћења као почетну страницу: ",
@ -104,62 +117,82 @@
"Administrator preferences": "Подешавања администратора",
"Default homepage: ": "Подразумевана главна страница: ",
"Feed menu: ": "Мени довода: ",
"Show nickname on top: ": "",
"Top enabled: ": "",
"CAPTCHA enabled: ": "",
"Login enabled: ": "",
"Registration enabled: ": "",
"CAPTCHA enabled: ": "CAPTCHA укључена?: ",
"Login enabled: ": "Пријава укључена?: ",
"Registration enabled: ": "Регистрација укључена?: ",
"Report statistics: ": "",
"Save preferences": "",
"Subscription manager": "",
"Token manager": "",
"Token": "",
"`x` subscriptions.": "",
"`x` tokens.": "",
"Import/export": "",
"unsubscribe": "",
"revoke": "",
"Subscriptions": "",
"`x` unseen notifications.": "",
"search": "",
"Log out": "",
"Released under the AGPLv3 by Omar Roth.": "",
"Source available here.": "",
"View JavaScript license information.": "",
"View privacy policy.": "",
"Trending": "",
"Public": "",
"Unlisted": "",
"Private": "",
"View all playlists": "",
"Updated `x` ago": "",
"Delete playlist `x`?": "",
"Delete playlist": "",
"Create playlist": "",
"Title": "",
"Playlist privacy": "",
"Editing playlist `x`": "",
"Watch on YouTube": "",
"Hide annotations": "",
"Show annotations": "",
"Genre: ": "",
"License: ": "",
"Save preferences": "Сачувај подешавања",
"Subscription manager": "Управљање праћењима",
"Token manager": "Управљање токенима",
"Token": "Токен",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x`праћења"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x`токена"
},
"Import/export": "Увези/извези",
"unsubscribe": "укини праћење",
"revoke": "опозови",
"Subscriptions": "Праћења",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` непрочитаних обавештења"
},
"search": "претрага",
"Log out": "Одјавите се",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Изворни код доступан овде.",
"View JavaScript license information.": "Прикажи информације о JavaScript лиценци.",
"View privacy policy.": "Прикажи извештај о приватности.",
"Trending": "У тренду",
"Public": "Јавно",
"Unlisted": "По позиву",
"Private": "Приватно",
"View all playlists": "Прикажи све плејлисте",
"Updated `x` ago": "Ажурирано пре `x`",
"Delete playlist `x`?": "Избриши плејлисту `x`?",
"Delete playlist": "Избриши плејлисту",
"Create playlist": "Направи плејлисту",
"Title": "Наслов",
"Playlist privacy": "Видљивост плејлисте",
"Editing playlist `x`": "Уређујете плејлисту `x`",
"Show more": "",
"Show less": "",
"Watch on YouTube": "Гледајте на YouTube-у",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "Сакриј анотације",
"Show annotations": "Прикажи анотације",
"Genre: ": "Жанр: ",
"License: ": "Лиценца: ",
"Family friendly? ": "",
"Wilson score: ": "",
"Engagement: ": "",
"Whitelisted regions: ": "",
"Blacklisted regions: ": "",
"Engagement: ": "Ангажовање: ",
"Whitelisted regions: ": "Дозвољене области: ",
"Blacklisted regions: ": "Забрањене области: ",
"Shared `x`": "",
"`x` views.": "",
"Premieres in `x`": "",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": "`x` прегледа"
},
"Premieres in `x`": "Емитује се уживо за `x`",
"Premieres `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.": "",
"View YouTube comments": "",
"View more comments on Reddit": "",
"View `x` comments.": "",
"View Reddit comments": "",
"Hide replies": "",
"Show replies": "",
"Incorrect password": "",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Здраво! Изгледа да је искључен JavaScript. Кликните овде да бисте приказали коментаре. Требаће мало дуже да се учитају.",
"View YouTube comments": "Прикажи коментаре са YouTube-а",
"View more comments on Reddit": "Прикажи још коментара на Reddit-у",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"View Reddit comments": "Прикажи коментаре са Reddit-а",
"Hide replies": "Сакриј одговоре",
"Show replies": "Прикажи одговоре",
"Incorrect password": "Неисправна лозинка",
"Quota exceeded, try again in a few hours": "",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "",
"Invalid TFA code": "",
@ -180,10 +213,16 @@
"This channel does not exist.": "",
"Could not get channel info.": "",
"Could not fetch comments": "",
"View `x` replies.": "",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` ago": "",
"Load more": "",
"`x` points.": "",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Could not create mix.": "",
"Empty playlist": "",
"Not a playlist.": "",
@ -301,15 +340,37 @@
"Yiddish": "",
"Yoruba": "",
"Zulu": "",
"`x` years.": "",
"`x` months.": "",
"`x` weeks.": "",
"`x` days.": "",
"`x` hours.": "",
"`x` minutes.": "",
"`x` seconds.": "",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Fallback comments: ": "",
"Popular": "",
"Search": "",
"Top": "",
"About": "",
"Rating: ": "",
@ -332,5 +393,35 @@
"Videos": "",
"Playlists": "",
"Community": "",
"Current version: ": "Тренутна верзија: "
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "Тренутна верзија: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` prenumeranter",
"`x` videos": "`x` videor",
"`x` playlists": "`x` spellistor",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` prenumeranter",
"": "`x` prenumeranter"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` videor",
"": "`x` videor"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` spellistor",
"": "`x` spellistor"
},
"LIVE": "LIVE",
"Shared `x` ago": "Delad `x` sedan",
"Unsubscribe": "Avprenumerera",
@ -68,6 +77,8 @@
"Fallback captions: ": "Ersättningsundertexter: ",
"Show related videos: ": "Visa relaterade videor? ",
"Show annotations by default: ": "Visa länkar-i-videon som förval? ",
"Automatically extend video description: ": "Förläng videobeskrivning automatiskt: ",
"Interactive 360 degree videos: ": "Interaktiva 360-gradervideos: ",
"Visual preferences": "Visuella inställningar",
"Player style: ": "Spelarstil: ",
"Dark mode: ": "Mörkt läge: ",
@ -75,6 +86,8 @@
"dark": "Mörkt",
"light": "Ljust",
"Thin mode: ": "Lättviktigt läge: ",
"Miscellaneous preferences": "Övriga inställningar",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Prenumerationsinställningar",
"Show annotations by default for subscribed channels: ": "Visa länkar-i-videor som förval för kanaler som prenumereras på? ",
"Redirect homepage to feed: ": "Omdirigera hemsida till flöde: ",
@ -104,6 +117,7 @@
"Administrator preferences": "Administratörsinställningar",
"Default homepage: ": "Förvald hemsida: ",
"Feed menu: ": "Flödesmeny: ",
"Show nickname on top: ": "Visa smeknamn överst: ",
"Top enabled: ": "Topp påslaget? ",
"CAPTCHA enabled: ": "CAPTCHA påslaget? ",
"Login enabled: ": "Inloggning påslaget? ",
@ -113,16 +127,25 @@
"Subscription manager": "Prenumerationshanterare",
"Token manager": "Åtkomst-token-hanterare",
"Token": "Åtkomst-token",
"`x` subscriptions": "`x` prenumerationer",
"`x` tokens": "`x` åtkomst-token",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` prenumerationer",
"": "`x` prenumerationer"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` åtkomst-token",
"": "`x` åtkomst-token"
},
"Import/export": "Importera/exportera",
"unsubscribe": "avprenumerera",
"revoke": "återkalla",
"Subscriptions": "Prenumerationer",
"`x` unseen notifications": "`x` osedda aviseringar",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` osedda aviseringar",
"": "`x` osedda aviseringar"
},
"search": "sök",
"Log out": "Logga ut",
"Released under the AGPLv3 by Omar Roth.": "Utgiven under AGPLv3-licens av Omar Roth.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Källkod tillgänglig här.",
"View JavaScript license information.": "Visa JavaScript-licensinformation.",
"View privacy policy.": "Visa privatlivspolicy.",
@ -138,7 +161,11 @@
"Title": "Titel",
"Playlist privacy": "Privatläge på spellista",
"Editing playlist `x`": "Redigerer spellistan `x`",
"Show more": "Visa mer",
"Show less": "Visa mindre",
"Watch on YouTube": "Titta på YouTube",
"Switch Invidious Instance": "Byt Invidious Instans",
"Broken? Try another Invidious Instance": "Trasig? Prova en annan Invidious Instance",
"Hide annotations": "Dölj länkar-i-video",
"Show annotations": "Visa länkar-i-video",
"Genre: ": "Genre: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Vitlistade regioner: ",
"Blacklisted regions: ": "Svartlistade regioner: ",
"Shared `x`": "Delade `x`",
"`x` views": "`x` visningar",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` visningar",
"": "`x` visningar"
},
"Premieres in `x`": "Premiär om `x`",
"Premieres `x`": "Premiär av `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.": "Hej. Det ser ut som att du har JavaScript avstängt. Klicka här för att visa kommentarer, ha i åtanke att nedladdning tar längre tid.",
"View YouTube comments": "Visa YouTube-kommentarer",
"View more comments on Reddit": "Visa flera kommentarer på Reddit",
"View `x` comments": "Visa `x` kommentarer",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Visa `x` kommentarer",
"": "Visa `x` kommentarer"
},
"View Reddit comments": "Visa Reddit-kommentarer",
"Hide replies": "Dölj svar",
"Show replies": "Visa svar",
@ -180,10 +213,16 @@
"This channel does not exist.": "Denna kanal finns inte.",
"Could not get channel info.": "Kunde inte hämta kanalinfo.",
"Could not fetch comments": "Kunde inte hämta kommentarer",
"View `x` replies": "Visa `x` svar",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Visa `x` svar",
"": "Visa `x` svar"
},
"`x` ago": "`x` sedan",
"Load more": "Ladda fler",
"`x` points": "`x` poäng",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` poäng",
"": "`x` poäng"
},
"Could not create mix.": "Kunde inte skapa mix.",
"Empty playlist": "Spellistan är tom",
"Not a playlist.": "Ogiltig spellista.",
@ -301,15 +340,37 @@
"Yiddish": "Jiddisch",
"Yoruba": "Yoruba",
"Zulu": "Zulu",
"`x` years": "`x` år",
"`x` months": "`x` månader",
"`x` weeks": "`x` veckor",
"`x` days": "`x` dagar",
"`x` hours": "`x` timmar",
"`x` minutes": "`x` minuter",
"`x` seconds": "`x` sekunder",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` år",
"": "`x` år"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` månader",
"": "`x` månader"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` veckor",
"": "`x` veckor"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dagar",
"": "`x` dagar"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` timmar",
"": "`x` timmar"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuter",
"": "`x` minuter"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` sekunder",
"": "`x` sekunder"
},
"Fallback comments: ": "Fallback-kommentarer: ",
"Popular": "Populärt",
"Search": "Sök",
"Top": "Topp",
"About": "Om",
"Rating: ": "Betyg: ",
@ -332,5 +393,35 @@
"Videos": "Videor",
"Playlists": "Spellistor",
"Community": "Gemenskap",
"Current version: ": "Nuvarande version: "
"relevance": "relevans",
"rating": "rankning",
"date": "datum",
"views": "visningar",
"content_type": "Typ",
"duration": "Varaktighet",
"features": "Funktioner",
"sort": "Sortera efter",
"hour": "timme",
"today": "idag",
"week": "vecka",
"month": "månad",
"year": "år",
"video": "video",
"channel": "kanal",
"playlist": "spellista",
"movie": "film",
"show": "tv-serie",
"hd": "hd",
"subtitles": "undertexter",
"creative_commons": "creative_commons",
"3d": "3d",
"live": "live",
"4k": "4k",
"location": "plats",
"hdr": "hdr",
"filter": "Filter",
"Current version: ": "Nuvarande version: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "Uppdatera",
"next_steps_error_message_go_to_youtube": "Gå till Youtube"
}

View File

@ -1,27 +1,34 @@
{
"`x` subscribers": "`x` abone",
"`x` videos": "`x` video",
"`x` playlists": "`x` çalma listesi",
"`x` subscribers.": "`x` abone.",
"`x` videos.": "`x` video.",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abone",
"": "`x` abone"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` video",
"": "`x` video"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` oynatma listesi",
"": "`x` oynatma listesi"
},
"LIVE": "CANLI",
"Shared `x` ago": "`x` önce paylaşıldı",
"Unsubscribe": "Abonelikten çık",
"Subscribe": "Abone ol",
"View channel on YouTube": "Kanalı YouTube'da görüntüle",
"View playlist on YouTube": "Çalma listesini YouTube'da görüntüle",
"View playlist on YouTube": "Oynatma listesini YouTube'da görüntüle",
"newest": "en yeni",
"oldest": "en eski",
"popular": "popüler",
"last": "son",
"Next page": "Sonraki sayfa",
"Previous page": "Önceki sayfa",
"Clear watch history?": "İzleme geçmisini temizle?",
"Clear watch history?": "İzleme geçmişi temizlensin mi?",
"New password": "Yeni parola",
"New passwords must match": "Yeni parolalar eşleşmek zorunda",
"Cannot change password for Google accounts": "Google hesapları için parola değiştirilemez",
"Authorize token?": "Jetonu yetkilendir?",
"Authorize token for `x`?": "`x` için jetonu yetkilendir?",
"Authorize token?": "Belirteç yetkilendirilsin mi?",
"Authorize token for `x`?": "`x` için belirteç yetkilendirilsin mi?",
"Yes": "Evet",
"No": "Hayır",
"Import and Export Data": "Verileri İçe ve Dışa Aktar",
@ -35,7 +42,7 @@
"Export subscriptions as OPML": "Abonelikleri OPML olarak dışa aktar",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Abonelikleri OPML olarak dışa aktar (NewPipe ve FreeTube için)",
"Export data as JSON": "Verileri JSON olarak dışa aktar",
"Delete account?": "Hesabı sil?",
"Delete account?": "Hesap silinsin mi?",
"History": "Geçmiş",
"An alternative front-end to YouTube": "YouTube için alternatif bir ön-yüz",
"JavaScript license information": "JavaScript lisans bilgileri",
@ -64,12 +71,14 @@
"Preferred video quality: ": "Tercih edilen video kalitesi: ",
"Player volume: ": "Oynatıcı ses seviyesi: ",
"Default comments: ": "Öntanımlı yorumlar: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Öntanımlı altyazılar: ",
"Fallback captions: ": "Yedek altyazılar: ",
"Show related videos: ": "İlgili videoları göster: ",
"Show annotations by default: ": "Öntanımlı olarak ek açıklamaları göster: ",
"Automatically extend video description: ": "Video açıklamasını otomatik olarak genişlet: ",
"Interactive 360 degree videos: ": "Etkileşimli 360 derece videolar: ",
"Visual preferences": "Görsel tercihler",
"Player style: ": "Oynatıcı biçimi: ",
"Dark mode: ": "Karanlık mod: ",
@ -77,6 +86,8 @@
"dark": "karanlık",
"light": "aydınlık",
"Thin mode: ": "İnce mod: ",
"Miscellaneous preferences": "Çeşitli tercihler",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Otomatik örnek yeniden yönlendirmesi (yedek: redirect.invidious.io): ",
"Subscription preferences": "Abonelik tercihleri",
"Show annotations by default for subscribed channels: ": "Abone olunan kanallar için ek açıklamaları öntanımlı olarak göster: ",
"Redirect homepage to feed: ": "Ana sayfayı akışa yönlendir: ",
@ -100,12 +111,13 @@
"Import/export data": "Verileri içe/dışa aktar",
"Change password": "Parolayı değiştir",
"Manage subscriptions": "Abonelikleri yönet",
"Manage tokens": "Jetonları yönet",
"Manage tokens": "Belirteçleri yönet",
"Watch history": "İzleme geçmişi",
"Delete account": "Hesap silme",
"Administrator preferences": "Yönetici tercihleri",
"Default homepage: ": "Öntanımlı ana sayfa: ",
"Feed menu: ": "Akış menüsü: ",
"Show nickname on top: ": "Takma adı üstte göster: ",
"Top enabled: ": "Top etkin: ",
"CAPTCHA enabled: ": "CAPTCHA etkin: ",
"Login enabled: ": "Oturum açma etkin: ",
@ -113,55 +125,70 @@
"Report statistics: ": "Rapor istatistikleri: ",
"Save preferences": "Tercihleri kaydet",
"Subscription manager": "Abonelik yöneticisi",
"`x` subscriptions": "`x` abonelik",
"`x` tokens": "`x` belirteç",
"Token manager": "Jeton yöneticisi",
"Token": "Jeton",
"`x` subscriptions.": "`x` abonelik.",
"`x` tokens.": "`x` jeton.",
"`x` unseen notifications": "`x` okunmamış bildirim",
"Token manager": "Belirteç yöneticisi",
"Token": "Belirteç",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonelik",
"": "`x` abonelik"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` belirteç",
"": "`x` belirteç"
},
"Import/export": "İçe/dışa aktar",
"unsubscribe": "abonelikten çık",
"revoke": "geri al",
"Subscriptions": "Abonelikler",
"`x` unseen notifications.": "`x` okunmamış bildirim.",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` okunmamış bildirim",
"": "`x` okunmamış bildirim"
},
"search": "ara",
"Log out": ıkış yap",
"Public": "Genel",
"Released under the AGPLv3 by Omar Roth.": "Omar Roth tarafından AGPLv3 altında yayımlandı.",
"Private": "Özel",
"View all playlists": "Tüm çalma listelerini görüntüle",
"Updated `x` ago": "`x` önce güncellendi",
"Delete playlist `x`?": "`x` çalma listesini sil?",
"Delete playlist": "Çalma listesini sil",
"Create playlist": "Çalma listesi oluştur",
"Title": "Başlık",
"Playlist privacy": "Çalma listesi gizliliği",
"Editing playlist `x`": "`x` çalma listesi düzenleniyor",
"Released under the AGPLv3 on Github.": "Github'da AGPLv3 altında yayınlandı.",
"Source available here.": "Kaynak kodları burada bulunabilir.",
"View JavaScript license information.": "JavaScript lisans bilgilerini görüntüle.",
"View privacy policy.": "Gizlilik politikasını görüntüle.",
"Trending": "Trendler",
"Public": "Genel",
"Unlisted": "Listelenmemiş",
"Private": "Özel",
"View all playlists": "Tüm oynatma listelerini görüntüle",
"Updated `x` ago": "`x` önce güncellendi",
"Delete playlist `x`?": "`x` oynatma listesi silinsin mi?",
"Delete playlist": "Oynatma listesini sil",
"Create playlist": "Oynatma listesi oluştur",
"Title": "Başlık",
"Playlist privacy": "Oynatma listesi gizliliği",
"Editing playlist `x`": "`x` oynatma listesi düzenleniyor",
"Show more": "Daha fazla göster",
"Show less": "Daha az göster",
"Watch on YouTube": "YouTube'da izle",
"Switch Invidious Instance": "Invidious Örneğini Değiştir",
"Broken? Try another Invidious Instance": "Bozuk mu? Başka bir Invidious örneğini deneyin",
"Hide annotations": "Ek açıklamaları gizle",
"Show annotations": "Ek açıklamaları göster",
"Genre: ": "Tür: ",
"License: ": "Lisans: ",
"Family friendly? ": "Aile için uygun? ",
"`x` views": "`x` görüntüleme",
"Family friendly? ": "Aile için uygun mu? ",
"Wilson score: ": "Wilson puanı: ",
"Engagement: ": "İzleyenlerin oy verme oranı: ",
"Whitelisted regions: ": "Beyaz listeye alınan bölgeler: ",
"Blacklisted regions: ": "Kara listeye alınan bölgeler: ",
"Shared `x`": "`x` paylaşıldı",
"`x` views.": "`x` izlenme.",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` görüntüleme",
"": "`x` görüntüleme"
},
"Premieres in `x`": "`x`içinde ilk gösterim",
"Premieres `x`": "`x` ilk gösterim",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Merhaba! JavaScript'i kapatmış gibi görünüyorsun. Yorumları görüntülemek için buraya tıkla, yüklenmelerinin biraz uzun sürebileceğini unutma.",
"View YouTube comments": "YouTube yorumlarını görüntüle",
"View more comments on Reddit": "Reddit'te daha fazla yorum görüntüle",
"View `x` comments": "`x` yorum görüntüle",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` yorumu görüntüle",
"": "`x` yorumu görüntüle"
},
"View Reddit comments": "Reddit yorumlarını görüntüle",
"Hide replies": "Cevapları gizle",
"Show replies": "Cevapları göster",
@ -180,29 +207,33 @@
"Password cannot be empty": "Parola boş olamaz",
"Password cannot be longer than 55 characters": "Parola 55 karakterden uzun olamaz",
"Please log in": "Lütfen oturum açın",
"View `x` replies": "`x` yanıtı görüntüle",
"Invidious Private Feed for `x`": "`x` için İnvidious Özel Akışı",
"channel:`x`": "kanal:`x`",
"`x` points": "`x` puan",
"Deleted or invalid channel": "Silinmiş ya da geçersiz kanal",
"This channel does not exist.": "Bu kanal mevcut değil.",
"Could not get channel info.": "Kanal bilgisi alınamadı.",
"Could not fetch comments": "Yorumlar alınamadı",
"View `x` replies.": "`x` yanıtı görüntüle.",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` yanıtı görüntüle",
"": "`x` yanıtı görüntüle"
},
"`x` ago": "`x` önce",
"Load more": "Daha fazla yükle",
"`x` points.": "`x` puan.",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` puan",
"": "`x` puan"
},
"Could not create mix.": "Mix oluşturulamadı.",
"Empty playlist": "Boş oynatma listesi",
"Not a playlist.": "Oynatma listesi değil.",
"Playlist does not exist.": "Oynatma listesi mevcut değil.",
"Could not pull trending pages.": "Trend sayfaları alınamıyor.",
"Hidden field \"challenge\" is a required field": "Gizli alan \"challenge\" zorunlu bir alandır",
"Hidden field \"token\" is a required field": "Gizli alan \"jeton\" zorunlu bir alandır",
"Hidden field \"token\" is a required field": "\"belirteç\" gizli alanı zorunlu bir alandır",
"Erroneous challenge": "Hatalı challenge",
"Erroneous token": "Hatalı jeton",
"Erroneous token": "Hatalı belirteç",
"No such user": "Böyle bir kullanıcı yok",
"Token is expired, please try again": "Jetonun süresi doldu, lütfen tekrar deneyin",
"Token is expired, please try again": "Belirtecin süresi doldu, lütfen tekrar deneyin",
"English": "İngilizce",
"English (auto-generated)": "İngilizce (otomatik oluşturuldu)",
"Afrikaans": "Afrikanca",
@ -309,15 +340,37 @@
"Yiddish": "Yiddiş",
"Yoruba": "Yoruba dili",
"Zulu": "Zuluca",
"`x` years": "`x` yıl",
"`x` months": "`x` ay",
"`x` weeks": "`x` hafta",
"`x` days": "`x` gün",
"`x` hours": "`x` saat",
"`x` minutes": "`x` dakika",
"`x` seconds": "`x` saniye",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` yıl",
"": "`x` yıl"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ay",
"": "`x` ay"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` hafta",
"": "`x` hafta"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` gün",
"": "`x` gün"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` saat",
"": "`x` saat"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` dakika",
"": "`x` dakika"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` saniye",
"": "`x` saniye"
},
"Fallback comments: ": "Yedek yorumlar: ",
"Popular": "Popüler",
"Search": "Ara",
"Top": "Enler",
"About": "Hakkında",
"Rating: ": "Değerlendirme: ",
@ -340,5 +393,35 @@
"Videos": "Videolar",
"Playlists": "Oynatma listeleri",
"Community": "Topluluk",
"Current version: ": "Şu anki sürüm: "
"relevance": "İlgi",
"rating": "Değerlendirme",
"date": "Yükleme tarihi",
"views": "Görüntüleme sayısı",
"content_type": "Tür",
"duration": "Süre",
"features": "Özellikler",
"sort": "Sıralama Ölçütü",
"hour": "Son Saat",
"today": "Bugün",
"week": "Bu hafta",
"month": "Bu ay",
"year": "Bu yıl",
"video": "Video",
"channel": "Kanal",
"playlist": "Oynatma listesi",
"movie": "Film",
"show": "Gösteri",
"hd": "HD",
"subtitles": "Alt yazılar",
"creative_commons": "Creative Commons",
"3d": "3B",
"live": "Canlı",
"4k": "4K",
"location": "Konum",
"hdr": "HDR",
"filter": "Filtrele",
"Current version: ": "Şu anki sürüm: ",
"next_steps_error_message": "Bundan sonra şunları denemelisiniz: ",
"next_steps_error_message_refresh": "Yenile",
"next_steps_error_message_go_to_youtube": "YouTube'a git"
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` підписників",
"`x` videos": "`x` відео",
"`x` playlists": "списки відтворення \"x\"",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` підписників",
"": "`x` підписників"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` відео",
"": "`x` відео"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "списки відтворення `x`",
"": "списки відтворення `x`"
},
"LIVE": "ПРЯМИЙ ЕФІР",
"Shared `x` ago": "Розміщено `x` назад",
"Unsubscribe": "Відписатися",
@ -68,6 +77,8 @@
"Fallback captions: ": "Запасна мова субтитрів: ",
"Show related videos: ": "Показувати схожі відео? ",
"Show annotations by default: ": "Завжди показувати анотації? ",
"Automatically extend video description: ": "",
"Interactive 360 degree videos: ": "",
"Visual preferences": "Налаштування сайту",
"Player style: ": "Стиль програвача: ",
"Dark mode: ": "Темне оформлення: ",
@ -75,6 +86,8 @@
"dark": "темна",
"light": "Світла",
"Thin mode: ": "Полегшене оформлення: ",
"Miscellaneous preferences": "",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Налаштування підписок",
"Show annotations by default for subscribed channels: ": "Завжди показувати анотації у відео каналів, на які ви підписані? ",
"Redirect homepage to feed: ": "Показувати відео з каналів, на які підписані, як головну сторінку: ",
@ -104,6 +117,7 @@
"Administrator preferences": "Адміністраторські налаштування",
"Default homepage: ": "Усталена домашня сторінка: ",
"Feed menu: ": "Меню потоку з відео: ",
"Show nickname on top: ": "",
"Top enabled: ": "Увімкнути топ відео? ",
"CAPTCHA enabled: ": "Увімкнути капчу? ",
"Login enabled: ": "Увімкнути авторизацію? ",
@ -113,16 +127,25 @@
"Subscription manager": "Менеджер підписок",
"Token manager": "Менеджер токенів",
"Token": "Токен",
"`x` subscriptions": "`x` підписка / підписок / підписки",
"`x` tokens": "`x` токенів",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` підписка / підписок / підписки",
"": "`x` підписка / підписок / підписки"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` токенів",
"": "`x` токенів"
},
"Import/export": "Імпорт і експорт",
"unsubscribe": "відписатися",
"revoke": "скасувати",
"Subscriptions": "Підписки",
"`x` unseen notifications": "`x` непереглянуте сповіщення / непереглянутих сповіщень / непереглянутих сповіщення",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` непереглянуте сповіщення / непереглянутих сповіщень / непереглянутих сповіщення",
"": "`x` непереглянуте сповіщення / непереглянутих сповіщень / непереглянутих сповіщення"
},
"search": "пошук",
"Log out": "Вийти",
"Released under the AGPLv3 by Omar Roth.": "Реалізовано Омаром Ротом за ліцензією AGPLv3.",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Програмний код доступний тут.",
"View JavaScript license information.": "Переглянути інформацію щодо ліцензії JavaScript.",
"View privacy policy.": "Переглянути політику приватності.",
@ -138,7 +161,11 @@
"Title": "Заголовок",
"Playlist privacy": "Конфіденційність списку відтворення",
"Editing playlist `x`": "Редагування списку відтворення \"x\"",
"Show more": "",
"Show less": "",
"Watch on YouTube": "Дивитися на YouTube",
"Switch Invidious Instance": "",
"Broken? Try another Invidious Instance": "",
"Hide annotations": "Приховати анотації",
"Show annotations": "Показати анотації",
"Genre: ": "Жанр: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "Доступно у регіонах: ",
"Blacklisted regions: ": "Недоступно у регіонах: ",
"Shared `x`": "Розміщено `x`",
"`x` views": "`x` переглядів",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` переглядів",
"": "`x` переглядів"
},
"Premieres in `x`": "Прем’єра через `x`",
"Premieres `x`": "Прем’єра `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.": "Схоже, у вас відключений JavaScript. Щоб побачити коментарі, натисніть сюда, але майте на увазі, що вони можуть завантажуватися трохи довше.",
"View YouTube comments": "Переглянути коментарі з YouTube",
"View more comments on Reddit": "Переглянути більше коментарів на Reddit",
"View `x` comments": "Переглянути `x` коментар / коментарів / коментаря",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Переглянути `x` коментар / коментарів / коментаря",
"": "Переглянути `x` коментар / коментарів / коментаря"
},
"View Reddit comments": "Переглянути коментарі з Reddit",
"Hide replies": "Сховати відповіді",
"Show replies": "Показати відповіді",
@ -180,10 +213,16 @@
"This channel does not exist.": "Такого каналу не існує.",
"Could not get channel info.": "Не вдається отримати інформацію щодо цього каналу.",
"Could not fetch comments": "Не вдається завантажити коментарі",
"View `x` replies": "Переглянути `x` відповідь / відповідей / відповіді",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Переглянути `x` відповідь / відповідей / відповіді",
"": "Переглянути `x` відповідь / відповідей / відповіді"
},
"`x` ago": "`x` тому",
"Load more": "Завантажити більше",
"`x` points": "`x` очко / очок / очка",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` очко / очок / очка",
"": "`x` очко / очок / очка"
},
"Could not create mix.": "Не вдається створити мікс.",
"Empty playlist": "Плейлист порожній",
"Not a playlist.": "Недійсний плейлист.",
@ -301,15 +340,37 @@
"Yiddish": "Їдиш",
"Yoruba": "Йоруба",
"Zulu": "Зулу",
"`x` years": "`x` років",
"`x` months": "`x` місяців",
"`x` weeks": "`x` тижнів",
"`x` days": "`x` днів",
"`x` hours": "`x` годин",
"`x` minutes": "`x` хвилин",
"`x` seconds": "`x` секунд",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` років",
"": "`x` років"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` місяців",
"": "`x` місяців"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` тижнів",
"": "`x` тижнів"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` днів",
"": "`x` днів"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` годин",
"": "`x` годин"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` хвилин",
"": "`x` хвилин"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` секунд",
"": "`x` секунд"
},
"Fallback comments: ": "Резервні коментарі: ",
"Popular": "Популярне",
"Search": "",
"Top": "Топ",
"About": "Про сайт",
"Rating: ": "Рейтинг: ",
@ -332,5 +393,35 @@
"Videos": "Відео",
"Playlists": "Плейлисти",
"Community": "Спільнота",
"Current version: ": "Поточна версія: "
}
"relevance": "",
"rating": "",
"date": "",
"views": "",
"content_type": "",
"duration": "",
"features": "",
"sort": "",
"hour": "",
"today": "",
"week": "",
"month": "",
"year": "",
"video": "",
"channel": "",
"playlist": "",
"movie": "",
"show": "",
"hd": "",
"subtitles": "",
"creative_commons": "",
"3d": "",
"live": "",
"4k": "",
"location": "",
"hdr": "",
"filter": "",
"Current version: ": "Поточна версія: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

427
locales/vi.json Normal file
View File

@ -0,0 +1,427 @@
{
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscribers",
"": "`x` subscribers"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` video",
"": ""
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"LIVE": "TRỰC TIẾP",
"Shared `x` ago": "Đã chia sẻ` x` trước",
"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",
"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",
"Cannot change password for Google accounts": "Không thể thay đổi mật khẩu cho tài khoản Google",
"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",
"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 sống động",
"Import YouTube subscriptions": "Nhập đăng ký YouTube",
"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)",
"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 data as JSON": "Xuất dữ liệu 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",
"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ý",
"Log in with Google": "Đăng nhập bằng Google",
"User ID": "Tên 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",
"Sign In": "Đăng nhập",
"Register": "Đăng ký",
"E-mail": "E-mail",
"Google verification code": "Mã xác minh của Google",
"Preferences": "Sở thích",
"Player preferences": "Tùy chọn người chơi",
"Always loop: ": "Luôn lặp lại: ",
"Autoplay: ": "Tự chạy: ",
"Play next by default: ": "Phát tiếp theo theo mặc định: ",
"Autoplay next video: ": "Tự động phát video tiếp theo: ",
"Listen by default: ": "Nghe theo mặc định: ",
"Proxy videos: ": "Video proxy: ",
"Default speed: ": "Tốc độ mặc định: ",
"Preferred video quality: ": "Chất lượng video ưa thích: ",
"Player volume: ": "Khối lượng trình phát: ",
"Default comments: ": "Nhận xét mặc định: ",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "Phụ đề mặc định: ",
"Fallback captions: ": "Phụ đề dự phòng: ",
"Show related videos: ": "Hiển thị các video có liên quan: ",
"Show annotations by default: ": "Hiển thị chú thích theo mặc định: ",
"Automatically extend video description: ": "Tự động mở rộng mô tả video: ",
"Interactive 360 degree videos: ": "Video 360 độ tương tác: ",
"Visual preferences": "Tùy chọn hình ảnh",
"Player style: ": "Phong cách người chơi: ",
"Dark mode: ": "Chế độ tối: ",
"Theme: ": "Chủ đề: ",
"dark": "tối",
"light": "ánh sáng",
"Thin mode: ": "Chế độ mỏng: ",
"Miscellaneous preferences": "Tùy chọn khác",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Chuyển hướng phiên bản tự động (dự phòng thành redirect.invidious.io): ",
"Subscription preferences": "Tùy chọn đăng ký",
"Show annotations by default for subscribed channels: ": "Hiển thị chú thích theo mặc định cho các kênh đã đăng ký: ",
"Redirect homepage to feed: ": "Chuyển hướng trang chủ đến nguồn cấp dữ liệu: ",
"Number of videos shown in feed: ": "Số lượng video được hiển thị trong nguồn cấp dữ liệu: ",
"Sort videos by: ": "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",
"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: ",
"Only show unwatched: ": "Chỉ hiển thị chưa xem: ",
"Only show notifications (if there are any): ": "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",
"Data preferences": "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",
"Delete account": "Xóa tài khoản",
"Administrator preferences": "Tùy chọn quản trị viên",
"Default homepage: ": "Trang chủ mặc định: ",
"Feed menu: ": "Menu nguồn cấp dữ liệu: ",
"Show nickname on top: ": "Hiển thị biệt hiệu ở trên cùng: ",
"Top enabled: ": "Đã bật hàng đầu: ",
"CAPTCHA enabled: ": "Đã bật CAPTCHA: ",
"Login enabled: ": "Đã bật đăng nhập: ",
"Registration enabled: ": "Đã bật đăng ký: ",
"Report statistics: ": "Báo cáo thống kê: ",
"Save preferences": "Lưu tùy chọn",
"Subscription manager": "Người quản lý đăng ký",
"Token manager": "Trình quản lý mã thông báo",
"Token": "Mã thông báo",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Import/export": "",
"unsubscribe": "",
"revoke": "",
"Subscriptions": "",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"search": "Tìm kiếm",
"Log out": "Đăng xuất",
"Released under the AGPLv3 on Github.": "",
"Source available here.": "Nguồn có sẵn ở đây.",
"View JavaScript license information.": "Xem thông tin giấy phép JavaScript.",
"View privacy policy.": "Xem chính sách bảo mật.",
"Trending": "Xu hướng",
"Public": "Công cộng",
"Unlisted": "Riêng tư",
"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",
"Delete playlist `x`?": "Xóa danh sách phát` x`?",
"Delete playlist": "Xóa danh sách phát",
"Create playlist": "Tạo danh sách phát",
"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",
"Watch on YouTube": "Xem trên YouTube",
"Switch Invidious Instance": "Chuyển phiên bản Invidious",
"Broken? Try another Invidious Instance": "Bị hỏng? Hãy thử một Phiên bản Invidious khác",
"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? ",
"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: ",
"Shared `x`": "Chia sẻ` x`",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Premieres in `x`": "",
"Premieres `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.": "",
"View YouTube comments": "",
"View more comments on Reddit": "",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"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",
"Quota exceeded, try again in a few hours": "Đã vượt quá hạn ngạch, hãy thử lại sau vài giờ nữa",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Không thể đăng nhập, hãy đảm bảo rằng xác thực hai yếu tố (Authenticator hoặc SMS) được bật.",
"Invalid TFA code": "Mã TFA không hợp lệ",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Đăng nhập không thành công. Điều này có thể là do xác thực hai yếu tố chưa được bật cho tài khoản của bạn.",
"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",
"User ID is a required field": "User ID là trường bắt buộc",
"Password is a required field": "Mật khẩu là trường bắt buộc",
"Wrong username or password": "Tên người dùng hoặc mật khẩu sai",
"Please sign in using 'Log in with Google'": "Vui lòng đăng nhập bằng 'Đăng nhập bằng Google'",
"Password cannot be empty": "Mật khẩu không được để trống",
"Password cannot be longer than 55 characters": "Mật khẩu không được dài hơn 55 ký tự",
"Please log in": "Xin vui lòng đăng nhập",
"Invidious Private Feed for `x`": "Nguồn cấp dữ liệu riêng tư Invidious cho` x`",
"channel:`x`": "kênh:` x`",
"Deleted or invalid channel": "Kênh đã xóa hoặc không hợp lệ",
"This channel does not exist.": "Kênh này không tồn tại.",
"Could not get channel info.": "Không thể tải thông tin kênh.",
"Could not fetch comments": "Không thể tìm nạp nhận xét",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` ago": "",
"Load more": "",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"Could not create mix.": "Không thể tạo kết hợp.",
"Empty playlist": "Danh sách phát trống",
"Not a playlist.": "Không phải danh sách phát.",
"Playlist does not exist.": "Danh sách phát không tồn tại.",
"Could not pull trending pages.": "Không thể kéo các trang thịnh hành.",
"Hidden field \"challenge\" is a required field": "Trường ẩn \"challenge\" là trường bắt buộc",
"Hidden field \"token\" is a required field": "Trường ẩn \"token\" là trường bắt buộc",
"Erroneous challenge": "Thử thách sai",
"Erroneous token": "Mã thông báo bị lỗi",
"No such user": "Không có người dùng như vậy",
"Token is expired, please try again": "Token đã hết hạn, vui lòng thử lại",
"English": "Tiếng Anh",
"English (auto-generated)": "Tiếng Anh (auto-generated))",
"Afrikaans": "Tiếng Afrikaans",
"Albanian": "Tiếng Albania",
"Amharic": "Amharic",
"Arabic": "Tiếng Ả Rập",
"Armenian": "Tiếng Armenia",
"Azerbaijani": "Azerbaijan",
"Bangla": "Bangla",
"Basque": "Tiếng Basque",
"Belarusian": "Người Belarus",
"Bosnian": "Tiếng Bosnia",
"Bulgarian": "Tiếng Bungari",
"Burmese": "Tiếng Miến Điện",
"Catalan": "Tiếng Catalan",
"Cebuano": "Cebuano",
"Chinese (Simplified)": "Tiếng Trung (Giản thể)",
"Chinese (Traditional)": "Truyền thống Trung Hoa)",
"Corsican": "Corsican",
"Croatian": "Tiếng Croatia",
"Czech": "Tiếng Séc",
"Danish": "Người Đan Mạch",
"Dutch": "Tiếng Hà Lan",
"Esperanto": "Quốc tế ngữ",
"Estonian": "Tiếng Estonia",
"Filipino": "Filipino",
"Finnish": "Tiếng Phần Lan",
"French": "Người 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",
"Hawaiian": "Tiếng Hawaii",
"Hebrew": "Tiếng Do Thái",
"Hindi": "Tiếng Hindi",
"Hmong": "Hmong",
"Hungarian": "Người Hungary",
"Icelandic": "Tiếng Iceland",
"Igbo": "Igbo",
"Indonesian": "Tiếng Indonesia",
"Irish": "Tiếng Ailen",
"Italian": "Người Ý",
"Japanese": "Tiếng Nhật",
"Javanese": "Tiếng Java",
"Kannada": "Tiếng Kannada",
"Kazakh": "Tiếng Kazakh",
"Khmer": "Tiếng Khmer",
"Korean": "Hàn Quốc",
"Kurdish": "Tiếng Kurd",
"Kyrgyz": "Kyrgyz",
"Lao": "Lào",
"Latin": "Latin",
"Latvian": "Tiếng Latvia",
"Lithuanian": "Tiếng Litva",
"Luxembourgish": "Tiếng Luxembourg",
"Macedonian": "Người Macedonian",
"Malagasy": "Malagasy",
"Malay": "Tiếng Mã Lai",
"Malayalam": "Tiếng Malayalam",
"Maltese": "Cây nho",
"Maori": "Tiếng Maori",
"Marathi": "Marathi",
"Mongolian": "Tiếng Mông Cổ",
"Nepali": "Tiếng Nepal",
"Norwegian Bokmål": "Tiếng Na Uy Bokmål",
"Nyanja": "Nyanja",
"Pashto": "Pashto",
"Persian": "Tiếng Ba Tư",
"Polish": "Đánh bóng",
"Portuguese": "Tiếng Bồ Đào Nha",
"Punjabi": "Punjabi",
"Romanian": "Tiếng Rumani",
"Russian": "Tiếng Nga",
"Samoan": "Samoan",
"Scottish Gaelic": "Tiếng Gaelic Scotland",
"Serbian": "Tiếng Serbia",
"Shona": "Shona",
"Sindhi": "Sindhi",
"Sinhala": "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 (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",
"Telugu": "Tiếng Telugu",
"Thai": "Tiếng Thái",
"Turkish": "Tiếng Thổ Nhĩ Kỳ",
"Ukrainian": "Tiếng Ukraina",
"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",
"Zulu": "Tiếng Zulu",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "",
"": ""
},
"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",
"Rating: ": "Xếp hạng: ",
"Language: ": "Ngôn ngữ: ",
"View as playlist": "Xem dưới dạng danh sách phát",
"Default": "Mặc định",
"Music": "Âm nhạc",
"Gaming": "Trò chơi",
"News": "Tin tức",
"Movies": "Phim",
"Download": "Tải xuống",
"Download as: ": "Tải tệp 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",
"Videos": "Video",
"Playlists": "Danh sách phát",
"Community": "Cộng đồng",
"relevance": "liên quan",
"rating": "Xếp hạng",
"date": "ngày",
"views": "lượt xem",
"content_type": "content_type",
"duration": "thời lượng",
"features": "đặc trưng",
"sort": "sắp xếp",
"hour": "giờ",
"today": "hôm nay",
"week": "tuần",
"month": "tháng",
"year": "năm",
"video": "video",
"channel": "kênh",
"playlist": "danh sách phát",
"movie": "bộ phim",
"show": "chỉ",
"hd": "hd",
"subtitles": "phụ đề",
"creative_commons": "Commons sáng tạo",
"3d": "3d",
"live": "trực tiếp",
"4k": "4k",
"location": "vị trí",
"hdr": "hdr",
"filter": "bộ lọc",
"Current version: ": "Phiên bản hiện tại: ",
"next_steps_error_message": "",
"next_steps_error_message_refresh": "",
"next_steps_error_message_go_to_youtube": ""
}

View File

@ -1,7 +1,16 @@
{
"`x` subscribers": "`x` 位订阅者",
"`x` videos": "`x` 个视频",
"`x` playlists": "`x` 个播放列表",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 位订阅者",
"": "`x` 位订阅者"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 个视频",
"": "`x` 个视频"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 个播放列表",
"": "`x` 个播放列表"
},
"LIVE": "直播",
"Shared `x` ago": "`x` 前分享",
"Unsubscribe": "取消订阅",
@ -11,7 +20,7 @@
"newest": "最新",
"oldest": "最老",
"popular": "时下流行",
"last": "",
"last": "上一个",
"Next page": "下一页",
"Previous page": "上一页",
"Clear watch history?": "清除观看历史?",
@ -68,6 +77,8 @@
"Fallback captions: ": "后备字幕语言: ",
"Show related videos: ": "是否显示相关视频: ",
"Show annotations by default: ": "是否默认显示视频注释: ",
"Automatically extend video description: ": "自动展开视频描述: ",
"Interactive 360 degree videos: ": "互动式 360 度视频: ",
"Visual preferences": "视觉选项",
"Player style: ": "播放器样式: ",
"Dark mode: ": "深色模式: ",
@ -75,6 +86,8 @@
"dark": "暗色",
"light": "亮色",
"Thin mode: ": "窄页模式: ",
"Miscellaneous preferences": "其他选项",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "自动实例重定向 (回退到redirect.invidious.io): ",
"Subscription preferences": "订阅设置",
"Show annotations by default for subscribed channels: ": "默认情况下显示已订阅频道的注释: ",
"Redirect homepage to feed: ": "跳转主页到 feed: ",
@ -104,6 +117,7 @@
"Administrator preferences": "管理员选项",
"Default homepage: ": "默认主页: ",
"Feed menu: ": "Feed 菜单: ",
"Show nickname on top: ": "在顶部显示昵称: ",
"Top enabled: ": "是否启用“热门视频”页: ",
"CAPTCHA enabled: ": "是否启用验证码: ",
"Login enabled: ": "是否启用登录: ",
@ -113,16 +127,25 @@
"Subscription manager": "订阅管理器",
"Token manager": "令牌管理器",
"Token": "令牌",
"`x` subscriptions": "`x` 个订阅",
"`x` tokens": "`x` 个令牌",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 个订阅",
"": "`x` 个订阅"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 个令牌",
"": "`x` 个令牌"
},
"Import/export": "导入/导出",
"unsubscribe": "取消订阅",
"revoke": "吊销",
"Subscriptions": "订阅",
"`x` unseen notifications": "`x` 条未读通知",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 条未读通知",
"": "`x` 条未读通知"
},
"search": "搜索",
"Log out": "登出",
"Released under the AGPLv3 by Omar Roth.": "由 Omar Roth 开发,以 AGPLv3 授权。",
"Released under the AGPLv3 on Github.": "依据 AGPLv3 许可证发布于 Github。",
"Source available here.": "源码可在此查看。",
"View JavaScript license information.": "查看 JavaScript 协议信息。",
"View privacy policy.": "查看隐私政策。",
@ -138,7 +161,11 @@
"Title": "标题",
"Playlist privacy": "播放列表隐私设置",
"Editing playlist `x`": "正在编辑播放列表 `x`",
"Show more": "显示更多",
"Show less": "显示较少",
"Watch on YouTube": "在 YouTube 观看",
"Switch Invidious Instance": "切换 Invidious 实例",
"Broken? Try another Invidious Instance": "无法正常工作? 尝试另一个 Invidious 实例",
"Hide annotations": "隐藏注释",
"Show annotations": "显示注释",
"Genre: ": "风格: ",
@ -149,13 +176,19 @@
"Whitelisted regions: ": "白名单地区: ",
"Blacklisted regions: ": "黑名单地区: ",
"Shared `x`": "`x`发布",
"`x` views": "`x` 播放",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 播放",
"": "`x` 次观看"
},
"Premieres in `x`": "首映于 `x` 后",
"Premieres `x`": "首映于 `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.": "你好!看起来你关闭了 JavaScript。点击这里阅读评论。注意它们加载的时间可能会稍长。",
"View YouTube comments": "查看 YouTube 评论",
"View more comments on Reddit": "在 Reddit 查看更多评论",
"View `x` comments": "查看 `x` 条评论",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "查看 `x` 条评论",
"": "查看 `x` 条评论"
},
"View Reddit comments": "查看 Reddit 评论",
"Hide replies": "隐藏回复",
"Show replies": "显示回复",
@ -180,10 +213,16 @@
"This channel does not exist.": "频道不存在。",
"Could not get channel info.": "无法获取频道信息。",
"Could not fetch comments": "无法获取评论",
"View `x` replies": "查看 `x` 条回复",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "查看 `x` 条回复",
"": "查看 `x` 条回复"
},
"`x` ago": "`x` 前",
"Load more": "加载更多",
"`x` points": "`x` 分",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 分",
"": "`x` 分"
},
"Could not create mix.": "无法创建合集。",
"Empty playlist": "空播放列表",
"Not a playlist.": "非播放列表。",
@ -301,15 +340,37 @@
"Yiddish": "意第绪语",
"Yoruba": "约鲁巴语",
"Zulu": "祖鲁语",
"`x` years": "`x` 年",
"`x` months": "`x` 月",
"`x` weeks": "`x` 周",
"`x` days": "`x` 天",
"`x` hours": "`x` 小时",
"`x` minutes": "`x` 分钟",
"`x` seconds": "`x` 秒",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 年",
"": "`x` 年"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 月",
"": "`x` 个月"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 周",
"": "`x` 周"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 天",
"": "`x` 天"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 小时",
"": "`x` 小时"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 分钟",
"": "`x` 分钟"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 秒",
"": "`x` 秒"
},
"Fallback comments: ": "后备评论: ",
"Popular": "热门频道",
"Search": "搜索",
"Top": "热门视频",
"About": "关于",
"Rating: ": "评分: ",
@ -332,5 +393,35 @@
"Videos": "视频",
"Playlists": "播放列表",
"Community": "社区",
"Current version: ": "当前版本: "
"relevance": "相关度",
"rating": "评分",
"date": "上传日期",
"views": "观看次数",
"content_type": "类型",
"duration": "持续时间",
"features": "功能",
"sort": "排序依据",
"hour": "上个小时",
"today": "今日",
"week": "本周",
"month": "本月",
"year": "今年",
"video": "视频",
"channel": "频道",
"playlist": "播放列表",
"movie": "电影",
"show": "真人秀",
"hd": "高清",
"subtitles": "字幕",
"creative_commons": "creative_commons 许可",
"3d": "3d",
"live": "直播",
"4k": "4k",
"location": "位置",
"hdr": "hdr",
"filter": "过滤器",
"Current version: ": "当前版本: ",
"next_steps_error_message": "在此之后你应尝试: ",
"next_steps_error_message_refresh": "刷新",
"next_steps_error_message_go_to_youtube": "转到 YouTube"
}

View File

@ -1,9 +1,16 @@
{
"`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個訂閱者",
"`x` subscribers.": "`x` 個訂閱者",
"`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 部影片",
"`x` videos.": "`x` 部影片",
"`x` playlists": "`x` 播放清單",
"`x` subscribers": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個訂閱者",
"": "`x` 個訂閱者"
},
"`x` videos": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 部影片",
"": "`x` 部影片"
},
"`x` playlists": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 播放清單",
"": "`x` 播放清單"
},
"LIVE": "直播",
"Shared `x` ago": "`x` 前分享",
"Unsubscribe": "取消訂閱",
@ -64,12 +71,14 @@
"Preferred video quality: ": "偏好的影片畫質: ",
"Player volume: ": "播放器音量: ",
"Default comments: ": "預設留言: ",
"youtube": "youtube",
"youtube": "YouTube",
"reddit": "reddit",
"Default captions: ": "預設字幕: ",
"Fallback captions: ": "汰退字幕: ",
"Show related videos: ": "顯示相關的影片: ",
"Show annotations by default: ": "預設顯示註釋: ",
"Automatically extend video description: ": "自動展開影片描述: ",
"Interactive 360 degree videos: ": "互動式 360 度影片: ",
"Visual preferences": "視覺偏好設定",
"Player style: ": "播放器樣式: ",
"Dark mode: ": "深色模式: ",
@ -77,6 +86,8 @@
"dark": "深色",
"light": "淺色",
"Thin mode: ": "精簡模式: ",
"Miscellaneous preferences": "其他偏好設定",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "自動站台重新導向(汰退至 redirect.invidious.io ",
"Subscription preferences": "訂閱偏好設定",
"Show annotations by default for subscribed channels: ": "預設為已訂閱的頻道顯示註釋: ",
"Redirect homepage to feed: ": "重新導向首頁至 feed ",
@ -106,6 +117,7 @@
"Administrator preferences": "管理員偏好設定",
"Default homepage: ": "預設首頁: ",
"Feed menu: ": "Feed 選單: ",
"Show nickname on top: ": "在頂部顯示暱稱: ",
"Top enabled: ": "頂部啟用: ",
"CAPTCHA enabled: ": "CAPTCHA 啟用: ",
"Login enabled: ": "啟用登入: ",
@ -115,19 +127,25 @@
"Subscription manager": "訂閱管理員",
"Token manager": "Token 管理員",
"Token": "Token",
"`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個訂閱",
"`x` subscriptions.": "`x` 個訂閱",
"`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` token",
"`x` tokens.": "`x` 個存取金鑰",
"`x` subscriptions": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個訂閱",
"": "`x` 個訂閱"
},
"`x` tokens": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` token",
"": "`x` 個存取金鑰"
},
"Import/export": "匯入/匯出",
"unsubscribe": "取消訂閱",
"revoke": "撤銷",
"Subscriptions": "訂閱",
"`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個未讀的通知",
"`x` unseen notifications.": "`x` 個未讀的通知",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個未讀的通知",
"": "`x` 個未讀的通知"
},
"search": "搜尋",
"Log out": "登出",
"Released under the AGPLv3 by Omar Roth.": "Omar Roth 以 AGPLv3 釋出。",
"Released under the AGPLv3 on Github.": "在 GitHub 上以 AGPLv3 釋出。",
"Source available here.": "原始碼在此提供。",
"View JavaScript license information.": "檢視 JavaScript 授權條款資訊。",
"View privacy policy.": "檢視隱私權政策。",
@ -143,7 +161,11 @@
"Title": "標題",
"Playlist privacy": "播放清單隱私",
"Editing playlist `x`": "已編輯播放清單 `x`",
"Show more": "顯示更多",
"Show less": "顯示較少",
"Watch on YouTube": "在 YouTube 上觀看",
"Switch Invidious Instance": "切換 Invidious 站台",
"Broken? Try another Invidious Instance": "故障了嗎?試試看其他 Invidious 站台吧",
"Hide annotations": "隱藏註釋",
"Show annotations": "顯示註釋",
"Genre: ": "風格: ",
@ -154,14 +176,19 @@
"Whitelisted regions: ": "白名單區域: ",
"Blacklisted regions: ": "黑名單區域: ",
"Shared `x`": "`x` 發佈",
"`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 次檢視",
"`x` views.": "`x` 次檢視",
"`x` views": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 次檢視",
"": "`x` 次檢視"
},
"Premieres in `x`": "首映於 `x`",
"Premieres `x`": "首映於 `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.": "嗨!看來您將 JavaScript 關閉了。點擊這裡以檢視留言,請注意,它們可能需要比較長的時間載入。",
"View YouTube comments": "檢視 YouTube 留言",
"View more comments on Reddit": "在 Reddit 上檢視更多留言",
"View `x` comments": "檢視 `x` 則留言",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "檢視 `x` 則留言",
"": "檢視 `x` 則留言"
},
"View Reddit comments": "檢視 Reddit 留言",
"Hide replies": "隱藏回覆",
"Show replies": "顯示回覆",
@ -186,12 +213,16 @@
"This channel does not exist.": "此頻道不存在。",
"Could not get channel info.": "無法取得頻道資訊。",
"Could not fetch comments": "無法擷取留言",
"View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "檢視 `x` 則回覆",
"View `x` replies.": "檢視 `x` 則回覆",
"View `x` replies": {
"([^.,0-9]|^)1([^.,0-9]|$)": "檢視 `x` 則回覆",
"": "檢視 `x` 則回覆"
},
"`x` ago": "`x` 以前",
"Load more": "載入更多",
"`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 點",
"`x` points.": "`x` 點",
"`x` points": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 點",
"": "`x` 點"
},
"Could not create mix.": "無法建立混合。",
"Empty playlist": "空的播放清單",
"Not a playlist.": "不是播放清單。",
@ -309,22 +340,37 @@
"Yiddish": "意第緒語",
"Yoruba": "約魯巴語",
"Zulu": "祖魯語",
"`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 年",
"`x` years.": "`x` 年",
"`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 月",
"`x` months.": "`x` 月",
"`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 週",
"`x` weeks.": "`x` 週",
"`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 天",
"`x` days.": "`x` 天",
"`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 小時",
"`x` hours.": "`x` 小時",
"`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 天",
"`x` minutes.": "`x` 分鐘",
"`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 秒",
"`x` seconds.": "`x` 秒",
"`x` years": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 年",
"": "`x` 年"
},
"`x` months": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 月",
"": "`x` 月"
},
"`x` weeks": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 週",
"": "`x` 週"
},
"`x` days": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 天",
"": "`x` 天"
},
"`x` hours": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 小時",
"": "`x` 小時"
},
"`x` minutes": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 天",
"": "`x` 分鐘"
},
"`x` seconds": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 秒",
"": "`x` 秒"
},
"Fallback comments: ": "汰退留言: ",
"Popular": "熱門頻道",
"Search": "搜尋",
"Top": "熱門影片",
"About": "關於",
"Rating: ": "評分: ",
@ -347,5 +393,35 @@
"Videos": "影片",
"Playlists": "播放清單",
"Community": "社群",
"Current version: ": "目前版本: "
"relevance": "關聯",
"rating": "評分",
"date": "日期",
"views": "檢視",
"content_type": "內容類型",
"duration": "時長",
"features": "特色",
"sort": "排序",
"hour": "小時",
"today": "今天",
"week": "週",
"month": "月",
"year": "年",
"video": "影片",
"channel": "頻道",
"playlist": "播放清單",
"movie": "電影",
"show": "秀",
"hd": "HD",
"subtitles": "字幕",
"creative_commons": "創用 CC",
"3d": "3D",
"live": "直播",
"4k": "4K",
"location": "位置",
"hdr": "HDR",
"filter": "篩選條件",
"Current version: ": "目前版本: ",
"next_steps_error_message": "之後您應該嘗試: ",
"next_steps_error_message_refresh": "重新整理",
"next_steps_error_message_go_to_youtube": "到 YouTube"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 89 KiB

23
scripts/git/pre-commit Normal file
View File

@ -0,0 +1,23 @@
# Useful precomit hooks
# Please see https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks for instructions on installation.
# Crystal linter
# This is a modified version of the pre-commit hook from the crystal repo. https://github.com/crystal-lang/crystal/blob/master/scripts/git/pre-commit
# Please refer to that if you'd like an version that doesn't automatically format staged files.
changed_cr_files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.cr$')
if [ ! -z "$changed_cr_files" ]; then
if [ -x bin/crystal ]; then
# use bin/crystal wrapper when available to run local compiler build
bin/crystal tool format $changed_cr_files >&2
else
crystal tool format $changed_cr_files >&2
fi
git add $changed_cr_files
fi
# Locale equalizer
if [ ! -z $(git diff --name-only --cached -- locales/) ]; then
crystal run scripts/propagate-new-locale-keys.cr
git add locales > /dev/null
fi

View File

@ -0,0 +1,95 @@
require "json"
require "../src/invidious/helpers/i18n.cr"
def locale_to_array(locale_name)
arrayifed_locale_data = [] of Tuple(String, JSON::Any | String)
keys_only_array = [] of String
LOCALES[locale_name].each do |k, v|
if v.as_h?
arrayifed_locale_data << {k, JSON.parse(v.as_h.to_json)}
elsif v.as_s?
arrayifed_locale_data << {k, v.as_s}
end
keys_only_array << k
end
return arrayifed_locale_data, keys_only_array
end
# Invidious currently has some unloaded localization files. We shouldn't need to propagate new keys onto those.
# We'll also remove the reference locale (english) from the list to process.
loaded_locales = LOCALES.keys.select! { |key| key != "en-US" }
english_locale, english_locale_keys = locale_to_array("en-US")
# In order to automatically propagate locale keys we're going to be needing two arrays.
# One is an array containing each locale data encoded as tuples. The other would contain
# sets of only the keys of each locale files.
#
# The second array is to make sure that an key from the english reference file is present
# in whatever the current locale we're scanning is.
locale_list = [] of Array(Tuple(String, JSON::Any | String))
locale_list_with_only_keys = [] of Array(String)
# Populates the created arrays from above
loaded_locales.each do |name|
arrayifed_locale_data, keys_only_locale = locale_to_array(name)
locale_list << arrayifed_locale_data
locale_list_with_only_keys << keys_only_locale
end
# Propagate additions
locale_list_with_only_keys.dup.each_with_index do |keys_of_locale_in_processing, index_of_locale_in_processing|
insert_at = {} of Int32 => Tuple(String, JSON::Any | String)
LOCALES["en-US"].each_with_index do |ref_locale_data, ref_locale_key_index|
ref_locale_key, ref_locale_value = ref_locale_data
# Found an new key that isn't present in the current locale..
if !keys_of_locale_in_processing.includes? ref_locale_key
# In terms of structure there's currently only two types; one for plural and the other for singular translations.
if ref_locale_value.as_h?
insert_at[ref_locale_key_index] = {ref_locale_key, JSON.parse({"([^.,0-9]|^)1([^.,0-9]|$)" => "", "" => ""}.to_json)}
else
insert_at[ref_locale_key_index] = {ref_locale_key, ""}
end
end
end
insert_at.each do |location_to_insert, data|
locale_list_with_only_keys[index_of_locale_in_processing].insert(location_to_insert, data[0])
locale_list[index_of_locale_in_processing].insert(location_to_insert, data)
end
end
# Propagate removals
locale_list_with_only_keys.dup.each_with_index do |keys_of_locale_in_processing, index_of_locale_in_processing|
remove_at = [] of Int32
keys_of_locale_in_processing.each_with_index do |current_key, current_key_index|
if !english_locale_keys.includes? current_key
remove_at << current_key_index
end
end
remove_at.each do |index_to_remove_at|
locale_list_with_only_keys[index_of_locale_in_processing].delete_at(index_to_remove_at)
locale_list[index_of_locale_in_processing].delete_at(index_to_remove_at)
end
end
# Now we convert back to our original format.
final_locale_list = [] of String
locale_list.each do |locale|
intermediate_hash = {} of String => (JSON::Any | String)
locale.each { |k, v| intermediate_hash[k] = v }
final_locale_list << intermediate_hash.to_pretty_json(indent = " ")
end
locale_map = Hash.zip(loaded_locales, final_locale_list)
# Export
locale_map.each do |locale_name, locale_contents|
File.write("locales/#{locale_name}.json", "#{locale_contents}\n")
end

View File

@ -1,40 +1,44 @@
version: 2.0
shards:
athena-negotiation:
git: https://github.com/athena-framework/negotiation.git
version: 0.1.1
backtracer:
git: https://github.com/sija/backtracer.cr.git
version: 1.2.1
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.10.0
version: 0.10.1
exception_page:
git: https://github.com/crystal-loot/exception_page.git
version: 0.1.4
version: 0.2.0
kemal:
git: https://github.com/kemalcr/kemal.git
version: 0.27.0
version: 1.1.0
kilt:
git: https://github.com/jeromegn/kilt.git
version: 0.4.0
version: 0.6.1
lsquic:
git: https://github.com/iv-org/lsquic.cr.git
version: 2.18.1-1
version: 2.18.1-2
pg:
git: https://github.com/will/crystal-pg.git
version: 0.23.1
pool:
git: https://github.com/ysbaddaden/pool.git
version: 0.2.3
version: 0.24.0
protodec:
git: https://github.com/omarroth/protodec.git
version: 0.1.3
git: https://github.com/iv-org/protodec.git
version: 0.1.4
radix:
git: https://github.com/luislavena/radix.git
version: 0.3.9
version: 0.4.1
sqlite3:
git: https://github.com/crystal-lang/crystal-sqlite3.git

View File

@ -12,23 +12,23 @@ targets:
dependencies:
pg:
github: will/crystal-pg
version: ~> 0.23.1
version: ~> 0.24.0
sqlite3:
github: crystal-lang/crystal-sqlite3
version: ~> 0.18.0
kemal:
github: kemalcr/kemal
version: ~> 0.27.0
pool:
github: ysbaddaden/pool
version: ~> 0.2.3
version: ~> 1.1.0
protodec:
github: omarroth/protodec
version: ~> 0.1.3
github: iv-org/protodec
version: ~> 0.1.4
lsquic:
github: iv-org/lsquic.cr
version: ~> 2.18.1-1
version: ~> 2.18.1-2
athena-negotiation:
github: athena-framework/negotiation
version: ~> 0.1.1
crystal: 0.36.1
crystal: ">= 1.0.0, < 2.0.0"
license: AGPLv3

View File

@ -5,7 +5,8 @@ require "protodec/utils"
require "spec"
require "yaml"
require "../src/invidious/helpers/*"
require "../src/invidious/channels"
require "../src/invidious/channels/*"
require "../src/invidious/videos"
require "../src/invidious/comments"
require "../src/invidious/playlists"
require "../src/invidious/search"
@ -27,11 +28,11 @@ describe "Helper" do
end
end
describe "#produce_channel_search_url" do
describe "#produce_channel_search_continuation" do
it "correctly produces token for searching a specific channel" do
produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("/browse_ajax?continuation=4qmFsgI2EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaGEVnWnpaV0Z5WTJnNEFYb0RNVEF3dUFFQVoA&gl=US&hl=en")
produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("/browse_ajax?continuation=4qmFsgJ0EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaGEVnWnpaV0Z5WTJnNEFYb0JNTGdCQUE9PVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr8%3D&gl=US&hl=en")
produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("4qmFsgKoARIYVUNYdXFTQmxIQUU2WHcteWVKQTBUdW53GiBFZ1p6WldGeVkyZ3dBVGdCWUFGNkJFZEJRVDI0QVFBPVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr-aAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
end
end
@ -41,25 +42,13 @@ describe "Helper" do
end
end
describe "#extract_channel_playlists_cursor" do
it "correctly extracts a playlists cursor from the given URL" do
extract_channel_playlists_cursor("4qmFsgLRARIYVUNDajk1NklGNjJGYlQ3R291c3phajl3GrQBRWdsd2JHRjViR2x6ZEhNWUF5QUJNQUk0QVdBQmFnQjZabEZWYkZCaE1XczFVbFpHZDJGV09XNWxWelI0V0RGR2VWSnVWbUZOV0Vwc1ZHcG5lRmd3TVU1aVZXdDRWMWN4YzFGdFNuTmtlbWh4VGpCd1NWTllVa1pTYTJNeFlVUmtlRmt3Y0ZWVWJWRXdWbnBzTkU1V1JqRmhNVGxFVm14dmQwMXFhRzVXZDdnQkFBJTNEJTNE", false).should eq("AIOkY9EQpi_gyn1_QrFuZ1reN81_MMmI1YmlBblw8j7JHItEFG5h7qcJTNd4W9x5Quk_CVZ028gW")
end
end
describe "#produce_playlist_continuation" do
it "correctly produces ctoken for requesting index `x` of a playlist" do
produce_playlist_continuation("UUCla9fZca4I7KagBtgRGnOw", 100).should eq("4qmFsgJNEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhhVVUNsYTlmWmNhNEk3S2FnQnRnUkduT3c%3D")
describe "#produce_playlist_url" do
it "correctly produces url for requesting index `x` of a playlist" do
produce_playlist_url("UUCla9fZca4I7KagBtgRGnOw", 0).should eq("/browse_ajax?continuation=4qmFsgIqEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoMZWdaUVZEcERRVUU9&gl=US&hl=en")
produce_playlist_continuation("UCCla9fZca4I7KagBtgRGnOw", 200).should eq("4qmFsgJLEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoSQ0FKNkIxQlVPa05OWjBJJTNEmgIYVVVDbGE5ZlpjYTRJN0thZ0J0Z1JHbk93")
produce_playlist_url("UCCla9fZca4I7KagBtgRGnOw", 0).should eq("/browse_ajax?continuation=4qmFsgIqEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoMZWdaUVZEcERRVUU9&gl=US&hl=en")
produce_playlist_url("PLt5AfwLFPxWLNVixpe1w3fi6lE2OTq0ET", 0).should eq("/browse_ajax?continuation=4qmFsgI0EiRWTFBMdDVBZndMRlB4V0xOVml4cGUxdzNmaTZsRTJPVHEwRVQaDGVnWlFWRHBEUVVFPQ%3D%3D&gl=US&hl=en")
produce_playlist_url("PLt5AfwLFPxWLNVixpe1w3fi6lE2OTq0ET", 10000).should eq("/browse_ajax?continuation=4qmFsgI0EiRWTFBMdDVBZndMRlB4V0xOVml4cGUxdzNmaTZsRTJPVHEwRVQaDGVnZFFWRHBEU2tKUA%3D%3D&gl=US&hl=en")
produce_playlist_url("PL55713C70BA91BD6E", 0).should eq("/browse_ajax?continuation=4qmFsgIkEhRWTFBMNTU3MTNDNzBCQTkxQkQ2RRoMZWdaUVZEcERRVUU9&gl=US&hl=en")
produce_playlist_url("PL55713C70BA91BD6E", 10000).should eq("/browse_ajax?continuation=4qmFsgIkEhRWTFBMNTU3MTNDNzBCQTkxQkQ2RRoMZWdkUVZEcERTa0pQ&gl=US&hl=en")
produce_playlist_continuation("PL55713C70BA91BD6E", 100).should eq("4qmFsgJBEhRWTFBMNTU3MTNDNzBCQTkxQkQ2RRoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhJQTDU1NzEzQzcwQkE5MUJENkU%3D")
end
end
@ -77,14 +66,6 @@ describe "Helper" do
end
end
describe "#extract_comment_cursor" do
it "correctly extracts a comment cursor from a given continuation" do
extract_comment_cursor("EiYSC2tKUVA3a2l3NUZrwAEByAEB4AEBogINKP___________wFAABgGMpwFCoYFQURTSl9pM1RqN1VlZ3dBd1daZkk4TmNiZ0djLVp0NFZEaW1BUGZwWHlPNDhuYUFxa3BsOXZYTk41OWpGXzNGRkVZeVpJOHRGWWpla0w1Z2ktcjhLdGFhcmduMDFxTUpsQ19QN2NaLWU5VGxxbTgzeUN6QVFHSUVtMGlMbUs5ZmVNOUVmNVo2S24xclpPRmlOdkxJS3JIUlJhWS10dkFNdzBDb0R3UWxiSXdpNDAzNkNCQ0ZXY2syemh1VHBsdEVUa2RmRHVrYVdkNnR1X1F4dkdnMGRkeEMydnNuVnlsQ1lJSUliWjAwMk1UTmpsbWJ5ejNKeGVybHJoa1drNW9kODZhOS16RVBPMjRHVzRKZnJlZEFvdGtzRmtCUUx5RWNRbkxRdHVyMHNwbGNmLUswZUttTlZkbk1DY1JVUF9LaU8tdVk4Qmg4RmtCa2RwMTFhVW10R0tzMWM0VjZXVkwwc29TallQc0VGLUF0LWlEVENJVXRNT1RLZklMblJ2V2NJclJvWndUNHA2MXFFMnhuN01CSFVJMzJJRjhJN2pKanh4a2o3ekMtUXBuT0xFdUNGOGJlN29kekFDa2VfTzVZNnpHM1FzN0lDM3NvV0NFbVJiLXlPNzB0ZDlXS3lXc25UNTJqM0FVT3hiQW16NU1EeU9qUVN3SERLNlFmaVh6N3ZjbGZnWEgxSUlqVmFCVUc3bkhlZkFOMlNoZ1BnN1hwaHBrV0FUdUtnRjNtRnBNRmViTFp2bHVPQ1k1WkgxVTh5LWV1ZnN5UUhxQkZJVlh0Mkg1NEFVa0xZeGdORmJTY0dfaEE4dEswV0JwdkdGUmE0V2dmT3NsNjlRSmRISTBKbWlOeS1rdyIPIgtrSlFQN2tpdzVGazAAKCg%3D").should eq("ADSJ_i3Tj7UegwAwWZfI8NcbgGc-Zt4VDimAPfpXyO48naAqkpl9vXNN59jF_3FFEYyZI8tFYjekL5gi-r8Ktaargn01qMJlC_P7cZ-e9Tlqm83yCzAQGIEm0iLmK9feM9Ef5Z6Kn1rZOFiNvLIKrHRRaY-tvAMw0CoDwQlbIwi4036CBCFWck2zhuTpltETkdfDukaWd6tu_QxvGg0ddxC2vsnVylCYIIIbZ002MTNjlmbyz3JxerlrhkWk5od86a9-zEPO24GW4JfredAotksFkBQLyEcQnLQtur0splcf-K0eKmNVdnMCcRUP_KiO-uY8Bh8FkBkdp11aUmtGKs1c4V6WVL0soSjYPsEF-At-iDTCIUtMOTKfILnRvWcIrRoZwT4p61qE2xn7MBHUI32IF8I7jJjxxkj7zC-QpnOLEuCF8be7odzACke_O5Y6zG3Qs7IC3soWCEmRb-yO70td9WKyWsnT52j3AUOxbAmz5MDyOjQSwHDK6QfiXz7vclfgXH1IIjVaBUG7nHefAN2ShgPg7XphpkWATuKgF3mFpMFebLZvluOCY5ZH1U8y-eufsyQHqBFIVXt2H54AUkLYxgNFbScG_hA8tK0WBpvGFRa4WgfOsl69QJdHI0JmiNy-kw")
extract_comment_cursor("EiYSC2tKUVA3a2l3NUZrwAEByAEB4AEBogINKP___________wFAABgGMo4DCvgCQURTSl9pMEhLLWg2SGRybURYZV93VXA3b1VuVmhFZlJtcUNndUxPaEtTNnlONURSdTAxZ2RQUVBEQkw3ZFVJci1fNDRPc3dVUDF0WjE1YVczMUJjN1JNb2ZCdzc0cDhyVnFLcWVzUDFPZnhOXzhDRlV2ZHo0aDlvalM1UzFJbjEzVGVXQkx5TmxlcHhRSy00Ymhwd1I0Q3FIN2I1YlBvMkw2ZE8xdklXc3VsRmJQQXpQb29XTkhPdGlHdlRsbmFybEl2VFBPb3BzcTFsd3RUanhSZ25yU0d2SlhscHFPeUpZb0tyR01Cam5nREk2ZFMxcTU2UEt1ajlvbTc4WTFvckhiZzhaOEZrNG54NUFDd2lCSjYtLTBoOXhpNnpSMi1oeTRnTTlGWnFIeHU1QlgwQzBCczJ0WEJ4V1BoTWVPVUtPVjh6UVFaOTNXdTlhc284THdPMVVJZmtkdWgxSTVMY0NaWUlPLXd1c1UxcnN5MWV5ekQtZ0NBTiIPIgtrSlFQN2tpdzVGazAAKCg%3D").should eq("ADSJ_i0HK-h6HdrmDXe_wUp7oUnVhEfRmqCguLOhKS6yN5DRu01gdPQPDBL7dUIr-_44OswUP1tZ15aW31Bc7RMofBw74p8rVqKqesP1OfxN_8CFUvdz4h9ojS5S1In13TeWBLyNlepxQK-4bhpwR4CqH7b5bPo2L6dO1vIWsulFbPAzPooWNHOtiGvTlnarlIvTPOopsq1lwtTjxRgnrSGvJXlpqOyJYoKrGMBjngDI6dS1q56PKuj9om78Y1orHbg8Z8Fk4nx5ACwiBJ6--0h9xi6zR2-hy4gM9FZqHxu5BX0C0Bs2tXBxWPhMeOUKOV8zQQZ93Wu9aso8LwO1UIfkduh1I5LcCZYIO-wusU1rsy1eyzD-gCAN")
end
end
describe "#produce_comment_continuation" do
it "correctly produces a continuation token for comments" do
produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA").should eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D")

File diff suppressed because it is too large Load Diff

View File

@ -1,984 +0,0 @@
struct InvidiousChannel
include DB::Serializable
property id : String
property author : String
property updated : Time
property deleted : Bool
property subscribed : Time?
end
struct ChannelVideo
include DB::Serializable
property id : String
property title : String
property published : Time
property updated : Time
property ucid : String
property author : String
property length_seconds : Int32 = 0
property live_now : Bool = false
property premiere_timestamp : Time? = nil
property views : Int64? = nil
def to_json(locale, json : JSON::Builder)
json.object do
json.field "type", "shortVideo"
json.field "title", self.title
json.field "videoId", self.id
json.field "videoThumbnails" do
generate_thumbnails(json, self.id)
end
json.field "lengthSeconds", self.length_seconds
json.field "author", self.author
json.field "authorId", self.ucid
json.field "authorUrl", "/channel/#{self.ucid}"
json.field "published", self.published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "viewCount", self.views
end
end
def to_json(locale, json : JSON::Builder | Nil = nil)
if json
to_json(locale, json)
else
JSON.build do |json|
to_json(locale, json)
end
end
end
def to_xml(locale, query_params, xml : XML::Builder)
query_params["v"] = self.id
xml.element("entry") do
xml.element("id") { xml.text "yt:video:#{self.id}" }
xml.element("yt:videoId") { xml.text self.id }
xml.element("yt:channelId") { xml.text self.ucid }
xml.element("title") { xml.text self.title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}")
xml.element("author") do
xml.element("name") { xml.text self.author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" }
end
xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg")
end
end
end
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("updated") { xml.text self.updated.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("media:group") do
xml.element("media:title") { xml.text self.title }
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg",
width: "320", height: "180")
end
end
end
def to_xml(locale, xml : XML::Builder | Nil = nil)
if xml
to_xml(locale, xml)
else
XML.build do |xml|
to_xml(locale, xml)
end
end
end
def to_tuple
{% begin %}
{
{{*@type.instance_vars.map { |var| var.name }}}
}
{% end %}
end
end
struct AboutRelatedChannel
include DB::Serializable
property ucid : String
property author : String
property author_url : String
property author_thumbnail : String
end
# TODO: Refactor into either SearchChannel or InvidiousChannel
struct AboutChannel
include DB::Serializable
property ucid : String
property author : String
property auto_generated : Bool
property author_url : String
property author_thumbnail : String
property banner : String?
property description_html : String
property paid : Bool
property total_views : Int64
property sub_count : Int32
property joined : Time
property is_family_friendly : Bool
property allowed_regions : Array(String)
property related_channels : Array(AboutRelatedChannel)
property tabs : Array(String)
end
class ChannelRedirect < Exception
property channel_id : String
def initialize(@channel_id)
end
end
def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, max_threads = 10)
finished_channel = Channel(String | Nil).new
spawn do
active_threads = 0
active_channel = Channel(Nil).new
channels.each do |ucid|
if active_threads >= max_threads
active_channel.receive
active_threads -= 1
end
active_threads += 1
spawn do
begin
get_channel(ucid, db, refresh, pull_all_videos)
finished_channel.send(ucid)
rescue ex
finished_channel.send(nil)
ensure
active_channel.send(nil)
end
end
end
end
final = [] of String
channels.size.times do
if ucid = finished_channel.receive
final << ucid
end
end
return final
end
def get_channel(id, db, refresh = true, pull_all_videos = true)
if channel = db.query_one?("SELECT * FROM channels WHERE id = $1", id, as: InvidiousChannel)
if refresh && Time.utc - channel.updated > 10.minutes
channel = fetch_channel(id, db, pull_all_videos: pull_all_videos)
channel_array = channel.to_a
args = arg_array(channel_array)
db.exec("INSERT INTO channels VALUES (#{args}) \
ON CONFLICT (id) DO UPDATE SET author = $2, updated = $3", args: channel_array)
end
else
channel = fetch_channel(id, db, pull_all_videos: pull_all_videos)
channel_array = channel.to_a
args = arg_array(channel_array)
db.exec("INSERT INTO channels VALUES (#{args})", args: channel_array)
end
return channel
end
def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
LOGGER.debug("fetch_channel: #{ucid}")
LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}, locale = #{locale}")
LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed")
rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body
LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed")
rss = XML.parse_html(rss)
author = rss.xpath_node(%q(//feed/title))
if !author
raise InfoException.new("Deleted or invalid channel")
end
author = author.content
# Auto-generated channels
# https://support.google.com/youtube/answer/2579942
if author.ends_with?(" - Topic") ||
{"Popular on YouTube", "Music", "Sports", "Gaming"}.includes? author
auto_generated = true
end
LOGGER.trace("fetch_channel: #{ucid} : author = #{author}, auto_generated = #{auto_generated}")
page = 1
LOGGER.trace("fetch_channel: #{ucid} : Downloading channel videos page")
response = get_channel_videos_response(ucid, page, auto_generated: auto_generated)
videos = [] of SearchVideo
begin
initial_data = JSON.parse(response.body)
raise InfoException.new("Could not extract channel JSON") if !initial_data
LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel videos page initial_data")
videos = extract_videos(initial_data.as_h, author, ucid)
rescue ex
if response.body.includes?("To continue with your YouTube experience, please fill out the form below.") ||
response.body.includes?("https://www.google.com/sorry/index")
raise InfoException.new("Could not extract channel info. Instance is likely blocked.")
end
raise ex
end
LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed")
rss.xpath_nodes("//feed/entry").each do |entry|
video_id = entry.xpath_node("videoid").not_nil!.content
title = entry.xpath_node("title").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)
author = entry.xpath_node("author/name").not_nil!.content
ucid = entry.xpath_node("channelid").not_nil!.content
views = entry.xpath_node("group/community/statistics").try &.["views"]?.try &.to_i64?
views ||= 0_i64
channel_video = videos.select { |video| video.id == video_id }[0]?
length_seconds = channel_video.try &.length_seconds
length_seconds ||= 0
live_now = channel_video.try &.live_now
live_now ||= false
premiere_timestamp = channel_video.try &.premiere_timestamp
video = ChannelVideo.new({
id: video_id,
title: title,
published: published,
updated: Time.utc,
ucid: ucid,
author: author,
length_seconds: length_seconds,
live_now: live_now,
premiere_timestamp: premiere_timestamp,
views: views,
})
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updating or inserting video")
# We don't include the 'premiere_timestamp' here because channel pages don't include them,
# meaning the above timestamp is always null
was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
if was_insert
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
db.exec("UPDATE users SET notifications = array_append(notifications, $1), \
feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid)
else
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
end
end
if pull_all_videos
page += 1
ids = [] of String
loop do
response = get_channel_videos_response(ucid, page, auto_generated: auto_generated)
initial_data = JSON.parse(response.body)
raise InfoException.new("Could not extract channel JSON") if !initial_data
videos = extract_videos(initial_data.as_h, author, ucid)
count = videos.size
videos = videos.map { |video| ChannelVideo.new({
id: video.id,
title: video.title,
published: video.published,
updated: Time.utc,
ucid: video.ucid,
author: video.author,
length_seconds: video.length_seconds,
live_now: video.live_now,
premiere_timestamp: video.premiere_timestamp,
views: video.views,
}) }
videos.each do |video|
ids << video.id
# We are notified of Red videos elsewhere (PubSub), which includes a correct published date,
# so since they don't provide a published date here we can safely ignore them.
if Time.utc - video.published > 1.minute
was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
db.exec("UPDATE users SET notifications = array_append(notifications, $1), \
feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert
end
end
break if count < 25
page += 1
end
end
channel = InvidiousChannel.new({
id: ucid,
author: author,
updated: Time.utc,
deleted: false,
subscribed: nil,
})
return channel
end
def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
if continuation || auto_generated
url = produce_channel_playlists_url(ucid, continuation, sort_by, auto_generated)
response = YT_POOL.client &.get(url)
continuation = response.body.match(/"continuation":"(?<continuation>[^"]+)"/).try &.["continuation"]?
initial_data = JSON.parse(response.body).as_a.find(&.["response"]?).try &.as_h
else
url = "/channel/#{ucid}/playlists?flow=list&view=1"
case sort_by
when "last", "last_added"
#
when "oldest", "oldest_created"
url += "&sort=da"
when "newest", "newest_created"
url += "&sort=dd"
else nil # Ignore
end
response = YT_POOL.client &.get(url)
continuation = response.body.match(/"continuation":"(?<continuation>[^"]+)"/).try &.["continuation"]?
initial_data = extract_initial_data(response.body)
end
return [] of SearchItem, nil if !initial_data
items = extract_items(initial_data)
continuation = extract_channel_playlists_cursor(continuation, auto_generated) if continuation
return items, continuation
end
def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:base64" => {
"2:string" => "videos",
"6:varint" => 2_i64,
"7:varint" => 1_i64,
"12:varint" => 1_i64,
"13:string" => "",
"23:varint" => 0_i64,
},
},
}
if !v2
if auto_generated
seed = Time.unix(1525757349)
until seed >= Time.utc
seed += 1.month
end
timestamp = seed - (page - 1).months
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0x36_i64
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = "#{timestamp.to_unix}"
else
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0_i64
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = "#{page}"
end
else
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0_i64
object["80226972:embedded"]["3:base64"].as(Hash)["61:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({
"1:string" => Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({
"1:varint" => 30_i64 * (page - 1),
}))),
})))
end
case sort_by
when "newest"
when "popular"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 0x01_i64
when "oldest"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 0x02_i64
else nil # Ignore
end
object["80226972:embedded"]["3:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json(object["80226972:embedded"]["3:base64"])))
object["80226972:embedded"].delete("3:base64")
continuation = object.try { |i| Protodec::Any.cast_json(object) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return continuation
end
def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
continuation = produce_channel_videos_continuation(ucid, page, auto_generated, sort_by, v2)
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end
def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false)
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:base64" => {
"2:string" => "playlists",
"6:varint" => 2_i64,
"7:varint" => 1_i64,
"12:varint" => 1_i64,
"13:string" => "",
"23:varint" => 0_i64,
},
},
}
if cursor
cursor = Base64.urlsafe_encode(cursor, false) if !auto_generated
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = cursor
end
if auto_generated
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0x32_i64
else
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 1_i64
case sort
when "oldest", "oldest_created"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 2_i64
when "newest", "newest_created"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 3_i64
when "last", "last_added"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 4_i64
else nil # Ignore
end
end
object["80226972:embedded"]["3:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json(object["80226972:embedded"]["3:base64"])))
object["80226972:embedded"].delete("3:base64")
continuation = object.try { |i| Protodec::Any.cast_json(object) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end
def extract_channel_playlists_cursor(cursor, auto_generated)
cursor = URI.decode_www_form(cursor)
.try { |i| Base64.decode(i) }
.try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) }
.try { |i| i["80226972:0:embedded"]["3:1:base64"].as_h.find { |k, v| k.starts_with? "15:" } }
.try &.[1]
if cursor.try &.as_h?
cursor = cursor.try { |i| Protodec::Any.cast_json(i.as_h) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } || ""
else
cursor = cursor.try &.as_s || ""
end
if !auto_generated
cursor = URI.decode_www_form(cursor)
.try { |i| Base64.decode_string(i) }
end
return cursor
end
# TODO: Add "sort_by"
def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en")
if response.status_code != 200
response = YT_POOL.client &.get("/user/#{ucid}/community?gl=US&hl=en")
end
if response.status_code != 200
raise InfoException.new("This channel does not exist.")
end
ucid = response.body.match(/https:\/\/www.youtube.com\/channel\/(?<ucid>UC[a-zA-Z0-9_-]{22})/).not_nil!["ucid"]
if !continuation || continuation.empty?
initial_data = extract_initial_data(response.body)
body = initial_data["contents"]?.try &.["twoColumnBrowseResultsRenderer"]["tabs"].as_a.select { |tab| tab["tabRenderer"]?.try &.["selected"].as_bool.== true }[0]?
if !body
raise InfoException.new("Could not extract community tab.")
end
body = body["tabRenderer"]["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]
else
continuation = produce_channel_community_continuation(ucid, continuation)
headers = HTTP::Headers.new
headers["cookie"] = response.cookies.add_request_headers(headers)["cookie"]
session_token = response.body.match(/"XSRF_TOKEN":"(?<session_token>[^"]+)"/).try &.["session_token"]? || ""
post_req = {
session_token: session_token,
}
response = YT_POOL.client &.post("/comment_service_ajax?action_get_comments=1&ctoken=#{continuation}&continuation=#{continuation}&hl=en&gl=US", headers, form: post_req)
body = JSON.parse(response.body)
body = body["response"]["continuationContents"]["itemSectionContinuation"]? ||
body["response"]["continuationContents"]["backstageCommentsContinuation"]?
if !body
raise InfoException.new("Could not extract continuation.")
end
end
continuation = body["continuations"]?.try &.[0]["nextContinuationData"]["continuation"].as_s
posts = body["contents"].as_a
if message = posts[0]["messageRenderer"]?
error_message = (message["text"]["simpleText"]? ||
message["text"]["runs"]?.try &.[0]?.try &.["text"]?)
.try &.as_s || ""
raise InfoException.new(error_message)
end
response = JSON.build do |json|
json.object do
json.field "authorId", ucid
json.field "comments" do
json.array do
posts.each do |post|
comments = post["backstagePostThreadRenderer"]?.try &.["comments"]? ||
post["backstageCommentsContinuation"]?
post = post["backstagePostThreadRenderer"]?.try &.["post"]["backstagePostRenderer"]? ||
post["commentThreadRenderer"]?.try &.["comment"]["commentRenderer"]?
next if !post
content_html = post["contentText"]?.try { |t| parse_content(t) } || ""
author = post["authorText"]?.try &.["simpleText"]? || ""
json.object do
json.field "author", author
json.field "authorThumbnails" do
json.array do
qualities = {32, 48, 76, 100, 176, 512}
author_thumbnail = post["authorThumbnail"]["thumbnails"].as_a[0]["url"].as_s
qualities.each do |quality|
json.object do
json.field "url", author_thumbnail.gsub(/s\d+-/, "s#{quality}-")
json.field "width", quality
json.field "height", quality
end
end
end
end
if post["authorEndpoint"]?
json.field "authorId", post["authorEndpoint"]["browseEndpoint"]["browseId"]
json.field "authorUrl", post["authorEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"].as_s
else
json.field "authorId", ""
json.field "authorUrl", ""
end
published_text = post["publishedTimeText"]["runs"][0]["text"].as_s
published = decode_date(published_text.rchop(" (edited)"))
if published_text.includes?(" (edited)")
json.field "isEdited", true
else
json.field "isEdited", false
end
like_count = post["actionButtons"]["commentActionButtonsRenderer"]["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"]
.try &.as_s.gsub(/\D/, "").to_i? || 0
json.field "content", html_to_content(content_html)
json.field "contentHtml", content_html
json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
json.field "likeCount", like_count
json.field "commentId", post["postId"]? || post["commentId"]? || ""
json.field "authorIsChannelOwner", post["authorEndpoint"]["browseEndpoint"]["browseId"] == ucid
if attachment = post["backstageAttachment"]?
json.field "attachment" do
json.object do
case attachment.as_h
when .has_key?("videoRenderer")
attachment = attachment["videoRenderer"]
json.field "type", "video"
if !attachment["videoId"]?
error_message = (attachment["title"]["simpleText"]? ||
attachment["title"]["runs"]?.try &.[0]?.try &.["text"]?)
json.field "error", error_message
else
video_id = attachment["videoId"].as_s
video_title = attachment["title"]["simpleText"]? || attachment["title"]["runs"]?.try &.[0]?.try &.["text"]?
json.field "title", video_title
json.field "videoId", video_id
json.field "videoThumbnails" do
generate_thumbnails(json, video_id)
end
json.field "lengthSeconds", decode_length_seconds(attachment["lengthText"]["simpleText"].as_s)
author_info = attachment["ownerText"]["runs"][0].as_h
json.field "author", author_info["text"].as_s
json.field "authorId", author_info["navigationEndpoint"]["browseEndpoint"]["browseId"]
json.field "authorUrl", author_info["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"]
# TODO: json.field "authorThumbnails", "channelThumbnailSupportedRenderers"
# TODO: json.field "authorVerified", "ownerBadges"
published = decode_date(attachment["publishedTimeText"]["simpleText"].as_s)
json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
view_count = attachment["viewCountText"]?.try &.["simpleText"].as_s.gsub(/\D/, "").to_i64? || 0_i64
json.field "viewCount", view_count
json.field "viewCountText", translate(locale, "`x` views", number_to_short_text(view_count))
end
when .has_key?("backstageImageRenderer")
attachment = attachment["backstageImageRenderer"]
json.field "type", "image"
json.field "imageThumbnails" do
json.array do
thumbnail = attachment["image"]["thumbnails"][0].as_h
width = thumbnail["width"].as_i
height = thumbnail["height"].as_i
aspect_ratio = (width.to_f / height.to_f)
url = thumbnail["url"].as_s.gsub(/=w\d+-h\d+(-p)?(-nd)?(-df)?(-rwa)?/, "=s640")
qualities = {320, 560, 640, 1280, 2000}
qualities.each do |quality|
json.object do
json.field "url", url.gsub(/=s\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", (quality / aspect_ratio).ceil.to_i
end
end
end
end
# TODO
# when .has_key?("pollRenderer")
# attachment = attachment["pollRenderer"]
# json.field "type", "poll"
else
json.field "type", "unknown"
json.field "error", "Unrecognized attachment type."
end
end
end
end
if comments && (reply_count = (comments["backstageCommentsRenderer"]["moreText"]["simpleText"]? ||
comments["backstageCommentsRenderer"]["moreText"]["runs"]?.try &.[0]?.try &.["text"]?)
.try &.as_s.gsub(/\D/, "").to_i?)
continuation = comments["backstageCommentsRenderer"]["continuations"]?.try &.as_a[0]["nextContinuationData"]["continuation"].as_s
continuation ||= ""
json.field "replies" do
json.object do
json.field "replyCount", reply_count
json.field "continuation", extract_channel_community_cursor(continuation)
end
end
end
end
end
end
end
if body["continuations"]?
continuation = body["continuations"][0]["nextContinuationData"]["continuation"].as_s
json.field "continuation", extract_channel_community_cursor(continuation)
end
end
end
if format == "html"
response = JSON.parse(response)
content_html = template_youtube_comments(response, locale, thin_mode)
response = JSON.build do |json|
json.object do
json.field "contentHtml", content_html
end
end
end
return response
end
def produce_channel_community_continuation(ucid, cursor)
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:string" => cursor || "",
},
}
continuation = object.try { |i| Protodec::Any.cast_json(object) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return continuation
end
def extract_channel_community_cursor(continuation)
object = URI.decode_www_form(continuation)
.try { |i| Base64.decode(i) }
.try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) }
.try { |i| i["80226972:0:embedded"]["3:1:base64"].as_h }
if object["53:2:embedded"]?.try &.["3:0:embedded"]?
object["53:2:embedded"]["3:0:embedded"]["2:0:string"] = object["53:2:embedded"]["3:0:embedded"]
.try { |i| i["2:0:base64"].as_h }
.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i, padding: false) }
object["53:2:embedded"]["3:0:embedded"].as_h.delete("2:0:base64")
end
cursor = Protodec::Any.cast_json(object)
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
cursor
end
def get_about_info(ucid, locale)
result = YT_POOL.client &.get("/channel/#{ucid}/about?gl=US&hl=en")
if result.status_code != 200
result = YT_POOL.client &.get("/user/#{ucid}/about?gl=US&hl=en")
end
if md = result.headers["location"]?.try &.match(/\/channel\/(?<ucid>UC[a-zA-Z0-9_-]{22})/)
raise ChannelRedirect.new(channel_id: md["ucid"])
end
if result.status_code != 200
raise InfoException.new("This channel does not exist.")
end
about = XML.parse_html(result.body)
if about.xpath_node(%q(//div[contains(@class, "channel-empty-message")]))
raise InfoException.new("This channel does not exist.")
end
initdata = extract_initial_data(result.body)
if initdata.empty?
error_message = about.xpath_node(%q(//div[@class="yt-alert-content"])).try &.content.strip
error_message ||= translate(locale, "Could not get channel info.")
raise InfoException.new(error_message)
end
if browse_endpoint = initdata["onResponseReceivedActions"]?.try &.[0]?.try &.["navigateAction"]?.try &.["endpoint"]?.try &.["browseEndpoint"]?
raise ChannelRedirect.new(channel_id: browse_endpoint["browseId"].to_s)
end
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
# Raises a KeyError on failure.
banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
banner = banners.try &.[-1]?.try &.["url"].as_s?
# if banner.includes? "channels/c4/default_banner"
# banner = nil
# end
description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || ""
description_html = HTML.escape(description).gsub("\n", "<br>")
paid = about.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True"
is_family_friendly = about.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True"
allowed_regions = about.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",")
related_channels = initdata["contents"]["twoColumnBrowseResultsRenderer"]
.["secondaryContents"]?.try &.["browseSecondaryContentsRenderer"]["contents"][0]?
.try &.["verticalChannelSectionRenderer"]?.try &.["items"]?.try &.as_a.map do |node|
renderer = node["miniChannelRenderer"]?
related_id = renderer.try &.["channelId"]?.try &.as_s?
related_id ||= ""
related_title = renderer.try &.["title"]?.try &.["simpleText"]?.try &.as_s?
related_title ||= ""
related_author_url = renderer.try &.["navigationEndpoint"]?.try &.["commandMetadata"]?.try &.["webCommandMetadata"]?
.try &.["url"]?.try &.as_s?
related_author_url ||= ""
related_author_thumbnails = renderer.try &.["thumbnail"]?.try &.["thumbnails"]?.try &.as_a?
related_author_thumbnails ||= [] of JSON::Any
related_author_thumbnail = ""
if related_author_thumbnails.size > 0
related_author_thumbnail = related_author_thumbnails[-1]["url"]?.try &.as_s?
related_author_thumbnail ||= ""
end
AboutRelatedChannel.new({
ucid: related_id,
author: related_title,
author_url: related_author_url,
author_thumbnail: related_author_thumbnail,
})
end
related_channels ||= [] of AboutRelatedChannel
total_views = 0_i64
joined = Time.unix(0)
tabs = [] of String
auto_generated = false
tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a?
if !tabs_json.nil?
# Retrieve information from the tabs array. The index we are looking for varies between channels.
tabs_json.each do |node|
# Try to find the about section which is located in only one of the tabs.
channel_about_meta = node["tabRenderer"]?.try &.["content"]?.try &.["sectionListRenderer"]?
.try &.["contents"]?.try &.[0]?.try &.["itemSectionRenderer"]?.try &.["contents"]?
.try &.[0]?.try &.["channelAboutFullMetadataRenderer"]?
if !channel_about_meta.nil?
total_views = channel_about_meta["viewCountText"]?.try &.["simpleText"]?.try &.as_s.gsub(/\D/, "").to_i64? || 0_i64
# The joined text is split to several sub strings. The reduce joins those strings before parsing the date.
joined = channel_about_meta["joinedDateText"]?.try &.["runs"]?.try &.as_a.reduce("") { |acc, node| acc + node["text"].as_s }
.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
# Auto-generated channels
# https://support.google.com/youtube/answer/2579942
# For auto-generated channels, channel_about_meta only has ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"]
if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) &&
(channel_about_meta["primaryLinks"][0]["title"]?.try &.["simpleText"]?.try &.as_s? || "") == "Auto-generated by YouTube"
auto_generated = true
end
end
end
tabs = tabs_json.reject { |node| node["tabRenderer"]?.nil? }.map { |node| node["tabRenderer"]["title"].as_s.downcase }
end
sub_count = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s?
.try { |text| short_text_to_number(text.split(" ")[0]) } || 0
AboutChannel.new({
ucid: ucid,
author: author,
auto_generated: auto_generated,
author_url: author_url,
author_thumbnail: author_thumbnail,
banner: banner,
description_html: description_html,
paid: paid,
total_views: total_views,
sub_count: sub_count,
joined: joined,
is_family_friendly: is_family_friendly,
allowed_regions: allowed_regions,
related_channels: related_channels,
tabs: tabs,
})
end
def get_channel_videos_response(ucid, page = 1, auto_generated = nil, sort_by = "newest", youtubei_browse = true)
if youtubei_browse
continuation = produce_channel_videos_continuation(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: true)
data = {
"context": {
"client": {
"clientName": "WEB",
"clientVersion": "2.20201021.03.00",
},
},
"continuation": continuation,
}.to_json
return YT_POOL.client &.post(
"/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
headers: HTTP::Headers{"content-type" => "application/json"},
body: data
)
else
url = produce_channel_videos_url(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: true)
return YT_POOL.client &.get(url)
end
end
def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest")
videos = [] of SearchVideo
2.times do |i|
response = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by)
initial_data = JSON.parse(response.body)
break if !initial_data
videos.concat extract_videos(initial_data.as_h, author, ucid)
end
return videos.size, videos
end
def get_latest_videos(ucid)
response = get_channel_videos_response(ucid)
initial_data = JSON.parse(response.body)
return [] of SearchVideo if !initial_data
author = initial_data["metadata"]?.try &.["channelMetadataRenderer"]?.try &.["title"]?.try &.as_s
items = extract_videos(initial_data.as_h, author, ucid)
return items
end

View File

@ -0,0 +1,174 @@
# TODO: Refactor into either SearchChannel or InvidiousChannel
struct AboutChannel
include DB::Serializable
property ucid : String
property author : String
property auto_generated : Bool
property author_url : String
property author_thumbnail : String
property banner : String?
property description_html : String
property total_views : Int64
property sub_count : Int32
property joined : Time
property is_family_friendly : Bool
property allowed_regions : Array(String)
property related_channels : Array(AboutRelatedChannel)
property tabs : Array(String)
end
struct AboutRelatedChannel
include DB::Serializable
property ucid : String
property author : String
property author_url : String
property author_thumbnail : String
end
def get_about_info(ucid, locale)
begin
# "EgVhYm91dA==" is the base64-encoded protobuf object {"2:string":"about"}
initdata = YoutubeAPI.browse(browse_id: ucid, params: "EgVhYm91dA==")
rescue
raise InfoException.new("Could not get channel info.")
end
if initdata.dig?("alerts", 0, "alertRenderer", "type") == "ERROR"
raise InfoException.new(initdata["alerts"][0]["alertRenderer"]["text"]["simpleText"].as_s)
end
if browse_endpoint = initdata["onResponseReceivedActions"]?.try &.[0]?.try &.["navigateAction"]?.try &.["endpoint"]?.try &.["browseEndpoint"]?
raise ChannelRedirect.new(channel_id: browse_endpoint["browseId"].to_s)
end
auto_generated = false
# Check for special auto generated gaming channels
if !initdata.has_key?("metadata")
auto_generated = true
end
if auto_generated
author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s
author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s
author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s
# Raises a KeyError on failure.
banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
banner = banners.try &.[-1]?.try &.["url"].as_s?
description = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"]["simpleText"].as_s
description_html = HTML.escape(description).gsub("\n", "<br>")
is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool
allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map { |a| a.as_s }
related_channels = [] of AboutRelatedChannel
else
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
# Raises a KeyError on failure.
banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
banner = banners.try &.[-1]?.try &.["url"].as_s?
# if banner.includes? "channels/c4/default_banner"
# banner = nil
# end
description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || ""
description_html = HTML.escape(description).gsub("\n", "<br>")
is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool
allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map { |a| a.as_s }
related_channels = initdata["contents"]["twoColumnBrowseResultsRenderer"]
.["secondaryContents"]?.try &.["browseSecondaryContentsRenderer"]["contents"][0]?
.try &.["verticalChannelSectionRenderer"]?.try &.["items"]?.try &.as_a.map do |node|
renderer = node["miniChannelRenderer"]?
related_id = renderer.try &.["channelId"]?.try &.as_s?
related_id ||= ""
related_title = renderer.try &.["title"]?.try &.["simpleText"]?.try &.as_s?
related_title ||= ""
related_author_url = renderer.try &.["navigationEndpoint"]?.try &.["commandMetadata"]?.try &.["webCommandMetadata"]?
.try &.["url"]?.try &.as_s?
related_author_url ||= ""
related_author_thumbnails = renderer.try &.["thumbnail"]?.try &.["thumbnails"]?.try &.as_a?
related_author_thumbnails ||= [] of JSON::Any
related_author_thumbnail = ""
if related_author_thumbnails.size > 0
related_author_thumbnail = related_author_thumbnails[-1]["url"]?.try &.as_s?
related_author_thumbnail ||= ""
end
AboutRelatedChannel.new({
ucid: related_id,
author: related_title,
author_url: related_author_url,
author_thumbnail: related_author_thumbnail,
})
end
related_channels ||= [] of AboutRelatedChannel
end
total_views = 0_i64
joined = Time.unix(0)
tabs = [] of String
tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a?
if !tabs_json.nil?
# Retrieve information from the tabs array. The index we are looking for varies between channels.
tabs_json.each do |node|
# Try to find the about section which is located in only one of the tabs.
channel_about_meta = node["tabRenderer"]?.try &.["content"]?.try &.["sectionListRenderer"]?
.try &.["contents"]?.try &.[0]?.try &.["itemSectionRenderer"]?.try &.["contents"]?
.try &.[0]?.try &.["channelAboutFullMetadataRenderer"]?
if !channel_about_meta.nil?
total_views = channel_about_meta["viewCountText"]?.try &.["simpleText"]?.try &.as_s.gsub(/\D/, "").to_i64? || 0_i64
# The joined text is split to several sub strings. The reduce joins those strings before parsing the date.
joined = channel_about_meta["joinedDateText"]?.try &.["runs"]?.try &.as_a.reduce("") { |acc, node| acc + node["text"].as_s }
.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
# Normal Auto-generated channels
# https://support.google.com/youtube/answer/2579942
# For auto-generated channels, channel_about_meta only has ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"]
if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) &&
(channel_about_meta["primaryLinks"][0]["title"]?.try &.["simpleText"]?.try &.as_s? || "") == "Auto-generated by YouTube"
auto_generated = true
end
end
end
tabs = tabs_json.reject { |node| node["tabRenderer"]?.nil? }.map { |node| node["tabRenderer"]["title"].as_s.downcase }
end
sub_count = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s?
.try { |text| short_text_to_number(text.split(" ")[0]) } || 0
AboutChannel.new({
ucid: ucid,
author: author,
auto_generated: auto_generated,
author_url: author_url,
author_thumbnail: author_thumbnail,
banner: banner,
description_html: description_html,
total_views: total_views,
sub_count: sub_count,
joined: joined,
is_family_friendly: is_family_friendly,
allowed_regions: allowed_regions,
related_channels: related_channels,
tabs: tabs,
})
end

View File

@ -0,0 +1,311 @@
struct InvidiousChannel
include DB::Serializable
property id : String
property author : String
property updated : Time
property deleted : Bool
property subscribed : Time?
end
struct ChannelVideo
include DB::Serializable
property id : String
property title : String
property published : Time
property updated : Time
property ucid : String
property author : String
property length_seconds : Int32 = 0
property live_now : Bool = false
property premiere_timestamp : Time? = nil
property views : Int64? = nil
def to_json(locale, json : JSON::Builder)
json.object do
json.field "type", "shortVideo"
json.field "title", self.title
json.field "videoId", self.id
json.field "videoThumbnails" do
generate_thumbnails(json, self.id)
end
json.field "lengthSeconds", self.length_seconds
json.field "author", self.author
json.field "authorId", self.ucid
json.field "authorUrl", "/channel/#{self.ucid}"
json.field "published", self.published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "viewCount", self.views
end
end
def to_json(locale, json : JSON::Builder | Nil = nil)
if json
to_json(locale, json)
else
JSON.build do |json|
to_json(locale, json)
end
end
end
def to_xml(locale, query_params, xml : XML::Builder)
query_params["v"] = self.id
xml.element("entry") do
xml.element("id") { xml.text "yt:video:#{self.id}" }
xml.element("yt:videoId") { xml.text self.id }
xml.element("yt:channelId") { xml.text self.ucid }
xml.element("title") { xml.text self.title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}")
xml.element("author") do
xml.element("name") { xml.text self.author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" }
end
xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg")
end
end
end
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("updated") { xml.text self.updated.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("media:group") do
xml.element("media:title") { xml.text self.title }
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg",
width: "320", height: "180")
end
end
end
def to_xml(locale, xml : XML::Builder | Nil = nil)
if xml
to_xml(locale, xml)
else
XML.build do |xml|
to_xml(locale, xml)
end
end
end
def to_tuple
{% begin %}
{
{{*@type.instance_vars.map { |var| var.name }}}
}
{% end %}
end
end
class ChannelRedirect < Exception
property channel_id : String
def initialize(@channel_id)
end
end
def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, max_threads = 10)
finished_channel = Channel(String | Nil).new
spawn do
active_threads = 0
active_channel = Channel(Nil).new
channels.each do |ucid|
if active_threads >= max_threads
active_channel.receive
active_threads -= 1
end
active_threads += 1
spawn do
begin
get_channel(ucid, db, refresh, pull_all_videos)
finished_channel.send(ucid)
rescue ex
finished_channel.send(nil)
ensure
active_channel.send(nil)
end
end
end
end
final = [] of String
channels.size.times do
if ucid = finished_channel.receive
final << ucid
end
end
return final
end
def get_channel(id, db, refresh = true, pull_all_videos = true)
if channel = db.query_one?("SELECT * FROM channels WHERE id = $1", id, as: InvidiousChannel)
if refresh && Time.utc - channel.updated > 10.minutes
channel = fetch_channel(id, db, pull_all_videos: pull_all_videos)
channel_array = channel.to_a
args = arg_array(channel_array)
db.exec("INSERT INTO channels VALUES (#{args}) \
ON CONFLICT (id) DO UPDATE SET author = $2, updated = $3", args: channel_array)
end
else
channel = fetch_channel(id, db, pull_all_videos: pull_all_videos)
channel_array = channel.to_a
args = arg_array(channel_array)
db.exec("INSERT INTO channels VALUES (#{args})", args: channel_array)
end
return channel
end
def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
LOGGER.debug("fetch_channel: #{ucid}")
LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}, locale = #{locale}")
LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed")
rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body
LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed")
rss = XML.parse_html(rss)
author = rss.xpath_node(%q(//feed/title))
if !author
raise InfoException.new("Deleted or invalid channel")
end
author = author.content
# Auto-generated channels
# https://support.google.com/youtube/answer/2579942
if author.ends_with?(" - Topic") ||
{"Popular on YouTube", "Music", "Sports", "Gaming"}.includes? author
auto_generated = true
end
LOGGER.trace("fetch_channel: #{ucid} : author = #{author}, auto_generated = #{auto_generated}")
page = 1
LOGGER.trace("fetch_channel: #{ucid} : Downloading channel videos page")
initial_data = get_channel_videos_response(ucid, page, auto_generated: auto_generated)
videos = extract_videos(initial_data, author, ucid)
LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed")
rss.xpath_nodes("//feed/entry").each do |entry|
video_id = entry.xpath_node("videoid").not_nil!.content
title = entry.xpath_node("title").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)
author = entry.xpath_node("author/name").not_nil!.content
ucid = entry.xpath_node("channelid").not_nil!.content
views = entry.xpath_node("group/community/statistics").try &.["views"]?.try &.to_i64?
views ||= 0_i64
channel_video = videos.select { |video| video.id == video_id }[0]?
length_seconds = channel_video.try &.length_seconds
length_seconds ||= 0
live_now = channel_video.try &.live_now
live_now ||= false
premiere_timestamp = channel_video.try &.premiere_timestamp
video = ChannelVideo.new({
id: video_id,
title: title,
published: published,
updated: Time.utc,
ucid: ucid,
author: author,
length_seconds: length_seconds,
live_now: live_now,
premiere_timestamp: premiere_timestamp,
views: views,
})
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updating or inserting video")
# We don't include the 'premiere_timestamp' here because channel pages don't include them,
# meaning the above timestamp is always null
was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
if was_insert
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
db.exec("UPDATE users SET notifications = array_append(notifications, $1), \
feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid)
else
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
end
end
if pull_all_videos
page += 1
ids = [] of String
loop do
initial_data = get_channel_videos_response(ucid, page, auto_generated: auto_generated)
videos = extract_videos(initial_data, author, ucid)
count = videos.size
videos = videos.map { |video| ChannelVideo.new({
id: video.id,
title: video.title,
published: video.published,
updated: Time.utc,
ucid: video.ucid,
author: video.author,
length_seconds: video.length_seconds,
live_now: video.live_now,
premiere_timestamp: video.premiere_timestamp,
views: video.views,
}) }
videos.each do |video|
ids << video.id
# We are notified of Red videos elsewhere (PubSub), which includes a correct published date,
# so since they don't provide a published date here we can safely ignore them.
if Time.utc - video.published > 1.minute
was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool)
db.exec("UPDATE users SET notifications = array_append(notifications, $1), \
feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert
end
end
break if count < 25
page += 1
end
end
channel = InvidiousChannel.new({
id: ucid,
author: author,
updated: Time.utc,
deleted: false,
subscribed: nil,
})
return channel
end

View File

@ -0,0 +1,275 @@
# TODO: Add "sort_by"
def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en")
if response.status_code != 200
response = YT_POOL.client &.get("/user/#{ucid}/community?gl=US&hl=en")
end
if response.status_code != 200
raise InfoException.new("This channel does not exist.")
end
ucid = response.body.match(/https:\/\/www.youtube.com\/channel\/(?<ucid>UC[a-zA-Z0-9_-]{22})/).not_nil!["ucid"]
if !continuation || continuation.empty?
initial_data = extract_initial_data(response.body)
body = initial_data["contents"]?.try &.["twoColumnBrowseResultsRenderer"]["tabs"].as_a.select { |tab| tab["tabRenderer"]?.try &.["selected"].as_bool.== true }[0]?
if !body
raise InfoException.new("Could not extract community tab.")
end
body = body["tabRenderer"]["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]
else
continuation = produce_channel_community_continuation(ucid, continuation)
headers = HTTP::Headers.new
headers["cookie"] = response.cookies.add_request_headers(headers)["cookie"]
session_token = response.body.match(/"XSRF_TOKEN":"(?<session_token>[^"]+)"/).try &.["session_token"]? || ""
post_req = {
session_token: session_token,
}
response = YT_POOL.client &.post("/comment_service_ajax?action_get_comments=1&ctoken=#{continuation}&continuation=#{continuation}&hl=en&gl=US", headers, form: post_req)
body = JSON.parse(response.body)
body = body["response"]["continuationContents"]["itemSectionContinuation"]? ||
body["response"]["continuationContents"]["backstageCommentsContinuation"]?
if !body
raise InfoException.new("Could not extract continuation.")
end
end
continuation = body["continuations"]?.try &.[0]["nextContinuationData"]["continuation"].as_s
posts = body["contents"].as_a
if message = posts[0]["messageRenderer"]?
error_message = (message["text"]["simpleText"]? ||
message["text"]["runs"]?.try &.[0]?.try &.["text"]?)
.try &.as_s || ""
raise InfoException.new(error_message)
end
response = JSON.build do |json|
json.object do
json.field "authorId", ucid
json.field "comments" do
json.array do
posts.each do |post|
comments = post["backstagePostThreadRenderer"]?.try &.["comments"]? ||
post["backstageCommentsContinuation"]?
post = post["backstagePostThreadRenderer"]?.try &.["post"]["backstagePostRenderer"]? ||
post["commentThreadRenderer"]?.try &.["comment"]["commentRenderer"]?
next if !post
content_html = post["contentText"]?.try { |t| parse_content(t) } || ""
author = post["authorText"]?.try &.["simpleText"]? || ""
json.object do
json.field "author", author
json.field "authorThumbnails" do
json.array do
qualities = {32, 48, 76, 100, 176, 512}
author_thumbnail = post["authorThumbnail"]["thumbnails"].as_a[0]["url"].as_s
qualities.each do |quality|
json.object do
json.field "url", author_thumbnail.gsub(/s\d+-/, "s#{quality}-")
json.field "width", quality
json.field "height", quality
end
end
end
end
if post["authorEndpoint"]?
json.field "authorId", post["authorEndpoint"]["browseEndpoint"]["browseId"]
json.field "authorUrl", post["authorEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"].as_s
else
json.field "authorId", ""
json.field "authorUrl", ""
end
published_text = post["publishedTimeText"]["runs"][0]["text"].as_s
published = decode_date(published_text.rchop(" (edited)"))
if published_text.includes?(" (edited)")
json.field "isEdited", true
else
json.field "isEdited", false
end
like_count = post["actionButtons"]["commentActionButtonsRenderer"]["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"]
.try &.as_s.gsub(/\D/, "").to_i? || 0
json.field "content", html_to_content(content_html)
json.field "contentHtml", content_html
json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
json.field "likeCount", like_count
json.field "commentId", post["postId"]? || post["commentId"]? || ""
json.field "authorIsChannelOwner", post["authorEndpoint"]["browseEndpoint"]["browseId"] == ucid
if attachment = post["backstageAttachment"]?
json.field "attachment" do
json.object do
case attachment.as_h
when .has_key?("videoRenderer")
attachment = attachment["videoRenderer"]
json.field "type", "video"
if !attachment["videoId"]?
error_message = (attachment["title"]["simpleText"]? ||
attachment["title"]["runs"]?.try &.[0]?.try &.["text"]?)
json.field "error", error_message
else
video_id = attachment["videoId"].as_s
video_title = attachment["title"]["simpleText"]? || attachment["title"]["runs"]?.try &.[0]?.try &.["text"]?
json.field "title", video_title
json.field "videoId", video_id
json.field "videoThumbnails" do
generate_thumbnails(json, video_id)
end
json.field "lengthSeconds", decode_length_seconds(attachment["lengthText"]["simpleText"].as_s)
author_info = attachment["ownerText"]["runs"][0].as_h
json.field "author", author_info["text"].as_s
json.field "authorId", author_info["navigationEndpoint"]["browseEndpoint"]["browseId"]
json.field "authorUrl", author_info["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"]
# TODO: json.field "authorThumbnails", "channelThumbnailSupportedRenderers"
# TODO: json.field "authorVerified", "ownerBadges"
published = decode_date(attachment["publishedTimeText"]["simpleText"].as_s)
json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
view_count = attachment["viewCountText"]?.try &.["simpleText"].as_s.gsub(/\D/, "").to_i64? || 0_i64
json.field "viewCount", view_count
json.field "viewCountText", translate(locale, "`x` views", number_to_short_text(view_count))
end
when .has_key?("backstageImageRenderer")
attachment = attachment["backstageImageRenderer"]
json.field "type", "image"
json.field "imageThumbnails" do
json.array do
thumbnail = attachment["image"]["thumbnails"][0].as_h
width = thumbnail["width"].as_i
height = thumbnail["height"].as_i
aspect_ratio = (width.to_f / height.to_f)
url = thumbnail["url"].as_s.gsub(/=w\d+-h\d+(-p)?(-nd)?(-df)?(-rwa)?/, "=s640")
qualities = {320, 560, 640, 1280, 2000}
qualities.each do |quality|
json.object do
json.field "url", url.gsub(/=s\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", (quality / aspect_ratio).ceil.to_i
end
end
end
end
# TODO
# when .has_key?("pollRenderer")
# attachment = attachment["pollRenderer"]
# json.field "type", "poll"
else
json.field "type", "unknown"
json.field "error", "Unrecognized attachment type."
end
end
end
end
if comments && (reply_count = (comments["backstageCommentsRenderer"]["moreText"]["simpleText"]? ||
comments["backstageCommentsRenderer"]["moreText"]["runs"]?.try &.[0]?.try &.["text"]?)
.try &.as_s.gsub(/\D/, "").to_i?)
continuation = comments["backstageCommentsRenderer"]["continuations"]?.try &.as_a[0]["nextContinuationData"]["continuation"].as_s
continuation ||= ""
json.field "replies" do
json.object do
json.field "replyCount", reply_count
json.field "continuation", extract_channel_community_cursor(continuation)
end
end
end
end
end
end
end
if body["continuations"]?
continuation = body["continuations"][0]["nextContinuationData"]["continuation"].as_s
json.field "continuation", extract_channel_community_cursor(continuation)
end
end
end
if format == "html"
response = JSON.parse(response)
content_html = template_youtube_comments(response, locale, thin_mode)
response = JSON.build do |json|
json.object do
json.field "contentHtml", content_html
end
end
end
return response
end
def produce_channel_community_continuation(ucid, cursor)
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:string" => cursor || "",
},
}
continuation = object.try { |i| Protodec::Any.cast_json(object) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return continuation
end
def extract_channel_community_cursor(continuation)
object = URI.decode_www_form(continuation)
.try { |i| Base64.decode(i) }
.try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) }
.try { |i| i["80226972:0:embedded"]["3:1:base64"].as_h }
if object["53:2:embedded"]?.try &.["3:0:embedded"]?
object["53:2:embedded"]["3:0:embedded"]["2:0:string"] = object["53:2:embedded"]["3:0:embedded"]
.try { |i| i["2:0:base64"].as_h }
.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i, padding: false) }
object["53:2:embedded"]["3:0:embedded"].as_h.delete("2:0:base64")
end
cursor = Protodec::Any.cast_json(object)
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
cursor
end

View File

@ -0,0 +1,93 @@
def fetch_channel_playlists(ucid, author, continuation, sort_by)
if continuation
response_json = YoutubeAPI.browse(continuation)
continuationItems = response_json["onResponseReceivedActions"]?
.try &.[0]["appendContinuationItemsAction"]["continuationItems"]
return [] of SearchItem, nil if !continuationItems
items = [] of SearchItem
continuationItems.as_a.select(&.as_h.has_key?("gridPlaylistRenderer")).each { |item|
extract_item(item, author, ucid).try { |t| items << t }
}
continuation = continuationItems.as_a.last["continuationItemRenderer"]?
.try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s
else
url = "/channel/#{ucid}/playlists?flow=list&view=1"
case sort_by
when "last", "last_added"
#
when "oldest", "oldest_created"
url += "&sort=da"
when "newest", "newest_created"
url += "&sort=dd"
else nil # Ignore
end
response = YT_POOL.client &.get(url)
initial_data = extract_initial_data(response.body)
return [] of SearchItem, nil if !initial_data
items = extract_items(initial_data, author, ucid)
continuation = response.body.match(/"token":"(?<continuation>[^"]+)"/).try &.["continuation"]?
end
return items, continuation
end
# ## NOTE: DEPRECATED
# Reason -> Unstable
# The Protobuf object must be provided with an id of the last playlist from the current "page"
# in order to fetch the next one accurately
# (if the id isn't included, entries shift around erratically between pages,
# leading to repetitions and skip overs)
#
# Since it's impossible to produce the appropriate Protobuf without an id being provided by the user,
# it's better to stick to continuation tokens provided by the first request and onward
def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false)
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:base64" => {
"2:string" => "playlists",
"6:varint" => 2_i64,
"7:varint" => 1_i64,
"12:varint" => 1_i64,
"13:string" => "",
"23:varint" => 0_i64,
},
},
}
if cursor
cursor = Base64.urlsafe_encode(cursor, false) if !auto_generated
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = cursor
end
if auto_generated
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0x32_i64
else
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 1_i64
case sort
when "oldest", "oldest_created"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 2_i64
when "newest", "newest_created"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 3_i64
when "last", "last_added"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 4_i64
else nil # Ignore
end
end
object["80226972:embedded"]["3:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json(object["80226972:embedded"]["3:base64"])))
object["80226972:embedded"].delete("3:base64")
continuation = object.try { |i| Protodec::Any.cast_json(object) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end

View File

@ -0,0 +1,89 @@
def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:base64" => {
"2:string" => "videos",
"6:varint" => 2_i64,
"7:varint" => 1_i64,
"12:varint" => 1_i64,
"13:string" => "",
"23:varint" => 0_i64,
},
},
}
if !v2
if auto_generated
seed = Time.unix(1525757349)
until seed >= Time.utc
seed += 1.month
end
timestamp = seed - (page - 1).months
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0x36_i64
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = "#{timestamp.to_unix}"
else
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0_i64
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = "#{page}"
end
else
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0_i64
object["80226972:embedded"]["3:base64"].as(Hash)["61:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({
"1:string" => Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({
"1:varint" => 30_i64 * (page - 1),
}))),
})))
end
case sort_by
when "newest"
when "popular"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 0x01_i64
when "oldest"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 0x02_i64
else nil # Ignore
end
object["80226972:embedded"]["3:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json(object["80226972:embedded"]["3:base64"])))
object["80226972:embedded"].delete("3:base64")
continuation = object.try { |i| Protodec::Any.cast_json(object) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return continuation
end
def get_channel_videos_response(ucid, page = 1, auto_generated = nil, sort_by = "newest")
continuation = produce_channel_videos_continuation(ucid, page,
auto_generated: auto_generated, sort_by: sort_by, v2: true)
return YoutubeAPI.browse(continuation)
end
def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest")
videos = [] of SearchVideo
2.times do |i|
initial_data = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by)
videos.concat extract_videos(initial_data, author, ucid)
end
return videos.size, videos
end
def get_latest_videos(ucid)
initial_data = get_channel_videos_response(ucid)
author = initial_data["metadata"]?.try &.["channelMetadataRenderer"]?.try &.["title"]?.try &.as_s
return extract_videos(initial_data, author, ucid)
end
# Used in bypass_captcha_job.cr
def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
continuation = produce_channel_videos_continuation(ucid, page, auto_generated, sort_by, v2)
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end

View File

@ -56,10 +56,7 @@ class RedditListing
property modhash : String
end
def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, sort_by = "top")
video = get_video(id, db, region: region)
session_token = video.session_token
def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_by = "top")
case cursor
when nil, ""
ctoken = produce_comment_continuation(id, cursor: "", sort_by: sort_by)
@ -71,38 +68,41 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so
ctoken = cursor
end
if !session_token
if format == "json"
return {"comments" => [] of String}.to_json
else
return {"contentHtml" => "", "commentCount" => 0}.to_json
client_config = YoutubeAPI::ClientConfig.new(region: region)
response = YoutubeAPI.next(continuation: ctoken, client_config: client_config)
contents = nil
if response["onResponseReceivedEndpoints"]?
onResponseReceivedEndpoints = response["onResponseReceivedEndpoints"]
header = nil
onResponseReceivedEndpoints.as_a.each do |item|
if item["reloadContinuationItemsCommand"]?
case item["reloadContinuationItemsCommand"]["slot"]
when "RELOAD_CONTINUATION_SLOT_HEADER"
header = item["reloadContinuationItemsCommand"]["continuationItems"][0]
when "RELOAD_CONTINUATION_SLOT_BODY"
contents = item["reloadContinuationItemsCommand"]["continuationItems"]
end
elsif item["appendContinuationItemsAction"]?
contents = item["appendContinuationItemsAction"]["continuationItems"]
end
end
end
post_req = {
page_token: ctoken,
session_token: session_token,
}
headers = HTTP::Headers{
"cookie" => video.cookie,
}
response = YT_POOL.client(region, &.post("/comment_service_ajax?action_get_comments=1&hl=en&gl=US&pbj=1", headers, form: post_req))
response = JSON.parse(response.body)
if !response["response"]["continuationContents"]?
elsif response["continuationContents"]?
response = response["continuationContents"]
if response["commentRepliesContinuation"]?
body = response["commentRepliesContinuation"]
else
body = response["itemSectionContinuation"]
end
contents = body["contents"]?
header = body["header"]?
if body["continuations"]?
moreRepliesContinuation = body["continuations"][0]["nextContinuationData"]["continuation"].as_s
end
else
raise InfoException.new("Could not fetch comments")
end
response = response["response"]["continuationContents"]
if response["commentRepliesContinuation"]?
body = response["commentRepliesContinuation"]
else
body = response["itemSectionContinuation"]
end
contents = body["contents"]?
if !contents
if format == "json"
return {"comments" => [] of String}.to_json
@ -111,13 +111,20 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so
end
end
continuationItemRenderer = nil
contents.as_a.reject! do |item|
if item["continuationItemRenderer"]?
continuationItemRenderer = item["continuationItemRenderer"]
true
end
end
response = JSON.build do |json|
json.object do
if body["header"]?
count_text = body["header"]["commentsHeaderRenderer"]["countText"]
if header
count_text = header["commentsHeaderRenderer"]["countText"]
comment_count = (count_text["simpleText"]? || count_text["runs"]?.try &.[0]?.try &.["text"]?)
.try &.as_s.gsub(/\D/, "").to_i? || 0
json.field "commentCount", comment_count
end
@ -127,7 +134,7 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so
json.array do
contents.as_a.each do |node|
json.object do
if !response["commentRepliesContinuation"]?
if node["commentThreadRenderer"]?
node = node["commentThreadRenderer"]
end
@ -135,7 +142,7 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so
node_replies = node["replies"]["commentRepliesRenderer"]
end
if !response["commentRepliesContinuation"]?
if node["comment"]?
node_comment = node["comment"]["commentRenderer"]
else
node_comment = node["commentRenderer"]
@ -180,12 +187,14 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so
json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
json.field "likeCount", node_comment["likeCount"]
comment_action_buttons_renderer = node_comment["actionButtons"]["commentActionButtonsRenderer"]
json.field "likeCount", comment_action_buttons_renderer["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"].as_s.scan(/\d/).map(&.[0]).join.to_i
json.field "commentId", node_comment["commentId"]
json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"]
if node_comment["actionButtons"]["commentActionButtonsRenderer"]["creatorHeart"]?
hearth_data = node_comment["actionButtons"]["commentActionButtonsRenderer"]["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"]
if comment_action_buttons_renderer["creatorHeart"]?
hearth_data = comment_action_buttons_renderer["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"]
json.field "creatorHeart" do
json.object do
json.field "creatorThumbnail", hearth_data["thumbnails"][-1]["url"]
@ -204,7 +213,11 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so
reply_count = 1
end
continuation = node_replies["continuations"]?.try &.as_a[0]["nextContinuationData"]["continuation"].as_s
if node_replies["continuations"]?
continuation = node_replies["continuations"]?.try &.as_a[0]["nextContinuationData"]["continuation"].as_s
elsif node_replies["contents"]?
continuation = node_replies["contents"]?.try &.as_a[0]["continuationItemRenderer"]["continuationEndpoint"]["continuationCommand"]["token"].as_s
end
continuation ||= ""
json.field "replies" do
@ -219,9 +232,15 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so
end
end
if body["continuations"]?
continuation = body["continuations"][0]["nextContinuationData"]["continuation"].as_s
json.field "continuation", cursor.try &.starts_with?("E") ? continuation : extract_comment_cursor(continuation)
if continuationItemRenderer
if continuationItemRenderer["continuationEndpoint"]?
continuationEndpoint = continuationItemRenderer["continuationEndpoint"]
elsif continuationItemRenderer["button"]?
continuationEndpoint = continuationItemRenderer["button"]["buttonRenderer"]["command"]
end
if continuationEndpoint
json.field "continuation", continuationEndpoint["continuationCommand"]["token"].as_s
end
end
end
end
@ -281,7 +300,7 @@ def fetch_reddit_comments(id, sort_by = "confidence")
return comments, thread
end
def template_youtube_comments(comments, locale, thin_mode)
def template_youtube_comments(comments, locale, thin_mode, is_replies = false)
String.build do |html|
root = comments["comments"].as_a
root.each do |child|
@ -292,7 +311,7 @@ def template_youtube_comments(comments, locale, thin_mode)
<div class="pure-u-23-24">
<p>
<a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}"
data-onclick="get_youtube_replies">#{translate(locale, "View `x` replies", number_with_separator(child["replies"]["replyCount"]))}</a>
data-onclick="get_youtube_replies" data-load-replies>#{translate(locale, "View `x` replies", number_with_separator(child["replies"]["replyCount"]))}</a>
</p>
</div>
</div>
@ -305,15 +324,17 @@ def template_youtube_comments(comments, locale, thin_mode)
author_thumbnail = ""
end
author_name = HTML.escape(child["author"].as_s)
html << <<-END_HTML
<div class="pure-g" style="width:100%">
<div class="channel-profile pure-u-4-24 pure-u-md-2-24">
<img style="padding-right:1em;padding-top:1em;width:90%" src="#{author_thumbnail}">
<img style="margin-right:1em;margin-top:1em;width:90%" src="#{author_thumbnail}">
</div>
<div class="pure-u-20-24 pure-u-md-22-24">
<p>
<b>
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{child["author"]}</a>
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{author_name}</a>
</b>
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
END_HTML
@ -412,7 +433,7 @@ def template_youtube_comments(comments, locale, thin_mode)
<div class="pure-u-1">
<p>
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
data-onclick="get_youtube_replies" data-load-more>#{translate(locale, "Load more")}</a>
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{translate(locale, "Load more")}</a>
</p>
</div>
</div>
@ -474,12 +495,16 @@ def replace_links(html)
html.xpath_nodes(%q(//a)).each do |anchor|
url = URI.parse(anchor["href"])
if {"www.youtube.com", "m.youtube.com", "youtu.be"}.includes?(url.host)
if url.path == "/redirect"
params = HTTP::Params.parse(url.query.not_nil!)
anchor["href"] = params["q"]?
if url.host.nil? || url.host.not_nil!.ends_with?("youtube.com") || url.host.not_nil!.ends_with?("youtu.be")
if url.host.try &.ends_with? "youtu.be"
url = "/watch?v=#{url.path.lstrip('/')}#{url.query_params}"
else
anchor["href"] = url.request_target
if url.path == "/redirect"
params = HTTP::Params.parse(url.query.not_nil!)
anchor["href"] = params["q"]?
else
anchor["href"] = url.request_target
end
end
elsif url.to_s == "#"
begin
@ -546,7 +571,9 @@ def content_to_comment_html(content)
if url = run["navigationEndpoint"]["urlEndpoint"]?.try &.["url"].as_s
url = URI.parse(url)
if !url.host || {"m.youtube.com", "www.youtube.com", "youtu.be"}.includes? url.host
if url.host == "youtu.be"
url = "/watch?v=#{url.request_target.lstrip('/')}"
elsif url.host.nil? || url.host.not_nil!.ends_with?("youtube.com")
if url.path == "/redirect"
url = HTTP::Params.parse(url.query.not_nil!)["q"]
else
@ -564,7 +591,7 @@ def content_to_comment_html(content)
else
text = %(<a href="/watch?v=#{video_id}">#{text}</a>)
end
elsif url = run["navigationEndpoint"]["commandMetadata"]?.try &.["webCommandMetadata"]["url"].as_s
elsif url = run.dig?("navigationEndpoint", "commandMetadata", "webCommandMetadata", "url").try &.as_s
text = %(<a href="#{url}">#{text}</a>)
end
end
@ -575,16 +602,6 @@ def content_to_comment_html(content)
return comment_html
end
def extract_comment_cursor(continuation)
cursor = URI.decode_www_form(continuation)
.try { |i| Base64.decode(i) }
.try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) }
.try { |i| i["6:2:embedded"]["1:0:string"].as_s }
return cursor
end
def produce_comment_continuation(video_id, cursor = "", sort_by = "top")
object = {
"2:embedded" => {

View File

@ -0,0 +1,70 @@
# Override of the TCPSocket and HTTP::Client classes in order to allow an
# IP family to be selected for domains that resolve to both IPv4 and
# IPv6 addresses.
#
class TCPSocket
def initialize(host : String, port, dns_timeout = nil, connect_timeout = nil, family = Socket::Family::UNSPEC)
Addrinfo.tcp(host, port, timeout: dns_timeout, family: family) do |addrinfo|
super(addrinfo.family, addrinfo.type, addrinfo.protocol)
connect(addrinfo, timeout: connect_timeout) do |error|
close
error
end
end
end
end
# :ditto:
class HTTP::Client
property family : Socket::Family = Socket::Family::UNSPEC
private def io
io = @io
return io if io
unless @reconnect
raise "This HTTP::Client cannot be reconnected"
end
hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host
io = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout, @family
io.read_timeout = @read_timeout if @read_timeout
io.write_timeout = @write_timeout if @write_timeout
io.sync = false
{% if !flag?(:without_openssl) %}
if tls = @tls
tcp_socket = io
begin
io = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host)
rescue exc
# don't leak the TCP socket when the SSL connection failed
tcp_socket.close
raise exc
end
end
{% end %}
@io = io
end
end
# Mute the ClientError exception raised when a connection is flushed.
# This happends when the connection is unexpectedly closed by the client.
#
class HTTP::Server::Response
class Output
private def unbuffered_flush
@io.flush
rescue ex : IO::Error
unbuffered_close
end
end
end
# TODO: Document this override
#
class PG::ResultSet
def field(index = @column_index)
@fields.not_nil![index]
end
end

View File

@ -40,6 +40,9 @@ def error_template_helper(env : HTTP::Server::Context, locale : Hash(String, JSO
and include the following text in your message:
<pre style="padding: 20px; background: rgba(0, 0, 0, 0.12345);">#{issue_template}</pre>
END_HTML
next_steps = error_redirect_helper(env, locale)
return templated "error"
end
@ -47,6 +50,7 @@ def error_template_helper(env : HTTP::Server::Context, locale : Hash(String, JSO
env.response.content_type = "text/html"
env.response.status_code = status_code
error_message = translate(locale, message)
next_steps = error_redirect_helper(env, locale)
return templated "error"
end
@ -103,3 +107,34 @@ end
def error_json_helper(env : HTTP::Server::Context, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String)
error_json_helper(env, locale, status_code, message, nil)
end
def error_redirect_helper(env : HTTP::Server::Context, locale : Hash(String, JSON::Any) | Nil)
request_path = env.request.path
if request_path.starts_with?("/search") || request_path.starts_with?("/watch") ||
request_path.starts_with?("/channel") || request_path.starts_with?("/playlist?list=PL")
next_steps_text = translate(locale, "next_steps_error_message")
refresh = translate(locale, "next_steps_error_message_refresh")
go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube")
switch_instance = translate(locale, "Switch Invidious Instance")
return <<-END_HTML
<p style="margin-bottom: 4px;">#{next_steps_text}</p>
<ul>
<li>
<a href="#{env.request.resource}">#{refresh}</a>
</li>
<li>
<a href="/redirect?referer=#{env.get("current_page")}">#{switch_instance}</a>
</li>
<li>
<a href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
</li>
</ul>
END_HTML
return next_step_html
else
return ""
end
end

View File

@ -0,0 +1,566 @@
# This file contains helper methods to parse the Youtube API json data into
# neat little packages we can use
# Tuple of Parsers/Extractors so we can easily cycle through them.
private ITEM_CONTAINER_EXTRACTOR = {
Extractors::YouTubeTabs,
Extractors::SearchResults,
Extractors::Continuation,
}
private ITEM_PARSERS = {
Parsers::VideoRendererParser,
Parsers::ChannelRendererParser,
Parsers::GridPlaylistRendererParser,
Parsers::PlaylistRendererParser,
Parsers::CategoryRendererParser,
}
record AuthorFallback, name : String, id : String
# Namespace for logic relating to parsing InnerTube data into various datastructs.
#
# Each of the parsers in this namespace are accessed through the #process() method
# which validates the given data as applicable to itself. If it is applicable the given
# data is passed to the private `#parse()` method which returns a datastruct of the given
# type. Otherwise, nil is returned.
private module Parsers
# Parses a InnerTube videoRenderer into a SearchVideo. Returns nil when the given object isn't a videoRenderer
#
# A videoRenderer renders a video to click on within the YouTube and Invidious UI. It is **not**
# the watchable video itself.
#
# See specs for example.
#
# `videoRenderer`s can be found almost everywhere on YouTube. In categories, search results, channels, etc.
#
module VideoRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = (item["videoRenderer"]? || item["gridVideoRenderer"]?)
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
video_id = item_contents["videoId"].as_s
title = extract_text(item_contents["title"]) || ""
# Extract author information
if author_info = item_contents.dig?("ownerText", "runs", 0)
author = author_info["text"].as_s
author_id = HelperExtractors.get_browse_id(author_info)
else
author = author_fallback.name
author_id = author_fallback.id
end
# For live videos (and possibly recently premiered videos) there is no published information.
# Instead, in its place is the amount of people currently watching. This behavior should be replicated
# on Invidious once all features of livestreams are supported. On an unrelated note, defaulting to the current
# time for publishing isn't a good idea.
published = item_contents.dig?("publishedTimeText", "simpleText").try { |t| decode_date(t.as_s) } || Time.local
# Typically views are stored under a "simpleText" in the "viewCountText". However, for
# livestreams and premiered it is stored under a "runs" array: [{"text":123}, {"text": "watching"}]
# When view count is disabled the "viewCountText" is not present on InnerTube data.
# TODO change default value to nil and typical encoding type to tuple storing type (watchers, views, etc)
# and count
view_count = item_contents.dig?("viewCountText", "simpleText").try &.as_s.gsub(/\D+/, "").to_i64? || 0_i64
description_html = item_contents["descriptionSnippet"]?.try { |t| parse_content(t) } || ""
# The length information *should* only always exist in "lengthText". However, the legacy Invidious code
# extracts from "thumbnailOverlays" when it doesn't. More testing is needed to see if this is
# actually needed
if length_container = item_contents["lengthText"]?
length_seconds = decode_length_seconds(length_container["simpleText"].as_s)
elsif length_container = item_contents["thumbnailOverlays"]?.try &.as_a.find(&.["thumbnailOverlayTimeStatusRenderer"]?)
length_seconds = extract_text(length_container["thumbnailOverlayTimeStatusRenderer"]["text"]).try { |t| decode_length_seconds(t) } || 0
else
length_seconds = 0
end
live_now = false
paid = false
premium = false
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
item_contents["badges"]?.try &.as_a.each do |badge|
b = badge["metadataBadgeRenderer"]
case b["label"].as_s
when "LIVE NOW"
live_now = true
when "New", "4K", "CC"
# TODO
when "Premium"
# TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"]
premium = true
else nil # Ignore
end
end
SearchVideo.new({
title: title,
id: video_id,
author: author,
ucid: author_id,
published: published,
views: view_count,
description_html: description_html,
length_seconds: length_seconds,
live_now: live_now,
premium: premium,
premiere_timestamp: premiere_timestamp,
})
end
end
# Parses a InnerTube channelRenderer into a SearchChannel. Returns nil when the given object isn't a channelRenderer
#
# A channelRenderer renders a channel to click on within the YouTube and Invidious UI. It is **not**
# the channel page itself.
#
# See specs for example.
#
# `channelRenderer`s can be found almost everywhere on YouTube. In categories, search results, channels, etc.
#
module ChannelRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = (item["channelRenderer"]? || item["gridChannelRenderer"]?)
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
author = extract_text(item_contents["title"]) || author_fallback.name
author_id = item_contents["channelId"]?.try &.as_s || author_fallback.id
author_thumbnail = HelperExtractors.get_thumbnails(item_contents)
# When public subscriber count is disabled, the subscriberCountText isn't sent by InnerTube.
# Always simpleText
# TODO change default value to nil
subscriber_count = item_contents.dig?("subscriberCountText", "simpleText")
.try { |s| short_text_to_number(s.as_s.split(" ")[0]) } || 0
# Auto-generated channels doesn't have videoCountText
# Taken from: https://github.com/iv-org/invidious/pull/2228#discussion_r717620922
auto_generated = item_contents["videoCountText"]?.nil?
video_count = HelperExtractors.get_video_count(item_contents)
description_html = item_contents["descriptionSnippet"]?.try { |t| parse_content(t) } || ""
SearchChannel.new({
author: author,
ucid: author_id,
author_thumbnail: author_thumbnail,
subscriber_count: subscriber_count,
video_count: video_count,
description_html: description_html,
auto_generated: auto_generated,
})
end
end
# Parses a InnerTube gridPlaylistRenderer into a SearchPlaylist. Returns nil when the given object isn't a gridPlaylistRenderer
#
# A gridPlaylistRenderer renders a playlist, that is located in a grid, to click on within the YouTube and Invidious UI.
# It is **not** the playlist itself.
#
# See specs for example.
#
# `gridPlaylistRenderer`s can be found on the playlist-tabs of channels and expanded categories.
#
module GridPlaylistRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["gridPlaylistRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
title = extract_text(item_contents["title"]) || ""
plid = item_contents["playlistId"]?.try &.as_s || ""
video_count = HelperExtractors.get_video_count(item_contents)
playlist_thumbnail = HelperExtractors.get_thumbnails(item_contents)
SearchPlaylist.new({
title: title,
id: plid,
author: author_fallback.name,
ucid: author_fallback.id,
video_count: video_count,
videos: [] of SearchPlaylistVideo,
thumbnail: playlist_thumbnail,
})
end
end
# Parses a InnerTube playlistRenderer into a SearchPlaylist. Returns nil when the given object isn't a playlistRenderer
#
# A playlistRenderer renders a playlist to click on within the YouTube and Invidious UI. It is **not** the playlist itself.
#
# See specs for example.
#
# `playlistRenderer`s can be found almost everywhere on YouTube. In categories, search results, recommended, etc.
#
module PlaylistRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["playlistRenderer"]?
return self.parse(item_contents)
end
end
private def self.parse(item_contents)
title = item_contents["title"]["simpleText"]?.try &.as_s || ""
plid = item_contents["playlistId"]?.try &.as_s || ""
video_count = HelperExtractors.get_video_count(item_contents)
playlist_thumbnail = HelperExtractors.get_thumbnails_plural(item_contents)
author_info = item_contents.dig("shortBylineText", "runs", 0)
author = author_info["text"].as_s
author_id = HelperExtractors.get_browse_id(author_info)
videos = item_contents["videos"]?.try &.as_a.map do |v|
v = v["childVideoRenderer"]
v_title = v.dig?("title", "simpleText").try &.as_s || ""
v_id = v["videoId"]?.try &.as_s || ""
v_length_seconds = v.dig?("lengthText", "simpleText").try { |t| decode_length_seconds(t.as_s) } || 0
SearchPlaylistVideo.new({
title: v_title,
id: v_id,
length_seconds: v_length_seconds,
})
end || [] of SearchPlaylistVideo
# TODO: item_contents["publishedTimeText"]?
SearchPlaylist.new({
title: title,
id: plid,
author: author,
ucid: author_id,
video_count: video_count,
videos: videos,
thumbnail: playlist_thumbnail,
})
end
end
# Parses a InnerTube shelfRenderer into a Category. Returns nil when the given object isn't a shelfRenderer
#
# A shelfRenderer renders divided sections on YouTube. IE "People also watched" in search results and
# the various organizational sections in the channel home page. A separate one (richShelfRenderer) is used
# for YouTube home. A shelfRenderer can also sometimes be expanded to show more content within it.
#
# See specs for example.
#
# `shelfRenderer`s can be found almost everywhere on YouTube. In categories, search results, channels, etc.
#
module CategoryRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["shelfRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
title = extract_text(item_contents["title"]?) || ""
url = item_contents.dig?("endpoint", "commandMetadata", "webCommandMetadata", "url")
.try &.as_s
# Sometimes a category can have badges.
badges = [] of Tuple(String, String) # (Badge style, label)
item_contents["badges"]?.try &.as_a.each do |badge|
badge = badge["metadataBadgeRenderer"]
badges << {badge["style"].as_s, badge["label"].as_s}
end
# Category description
description_html = item_contents["subtitle"]?.try { |desc| parse_content(desc) } || ""
# Content parsing
contents = [] of SearchItem
# Content could be in three locations.
if content_container = item_contents["content"]["horizontalListRenderer"]?
elsif content_container = item_contents["content"]["expandedShelfContentsRenderer"]?
elsif content_container = item_contents["content"]["verticalListRenderer"]?
else
content_container = item_contents["contents"]
end
raw_contents = content_container["items"].as_a
raw_contents.each do |item|
result = extract_item(item)
if !result.nil?
contents << result
end
end
Category.new({
title: title,
contents: contents,
description_html: description_html,
url: url,
badges: badges,
})
end
end
end
# The following are the extractors for extracting an array of items from
# the internal Youtube API's JSON response. The result is then packaged into
# a structure we can more easily use via the parsers above. Their internals are
# identical to the item parsers.
# Namespace for logic relating to extracting InnerTube's initial response to items we can parse.
#
# Each of the extractors in this namespace are accessed through the #process() method
# which validates the given data as applicable to itself. If it is applicable the given
# data is passed to the private `#extract()` method which returns an array of
# parsable items. Otherwise, nil is returned.
#
# NOTE perhaps the result from here should be abstracted into a struct in order to
# get additional metadata regarding the container of the item(s).
private module Extractors
# Extracts items from the selected YouTube tab.
#
# YouTube tabs are typically stored under "twoColumnBrowseResultsRenderer"
# and is structured like this:
#
# "twoColumnBrowseResultsRenderer": {
# {"tabs": [
# {"tabRenderer": {
# "endpoint": {...}
# "title": "Playlists",
# "selected": true,
# "content": {...},
# ...
# }}
# ]}
# }]
#
module YouTubeTabs
def self.process(initial_data : Hash(String, JSON::Any))
if target = initial_data["twoColumnBrowseResultsRenderer"]?
self.extract(target)
end
end
private def self.extract(target)
raw_items = [] of JSON::Any
content = extract_selected_tab(target["tabs"])["content"]
content["sectionListRenderer"]["contents"].as_a.each do |renderer_container|
renderer_container_contents = renderer_container["itemSectionRenderer"]["contents"][0]
# Category extraction
if items_container = renderer_container_contents["shelfRenderer"]?
raw_items << renderer_container_contents
next
elsif items_container = renderer_container_contents["gridRenderer"]?
else
items_container = renderer_container_contents
end
items_container["items"].as_a.each do |item|
raw_items << item
end
end
return raw_items
end
end
# Extracts items from the InnerTube response for search results
#
# Search results are typically stored under "twoColumnSearchResultsRenderer"
# and is structured like this:
#
# "twoColumnSearchResultsRenderer": {
# {"primaryContents": {
# {"sectionListRenderer": {
# "contents": [...],
# ...,
# "subMenu": {...},
# "hideBottomSeparator": true,
# "targetId": "search-feed"
# }}
# }}
# }
#
module SearchResults
def self.process(initial_data : Hash(String, JSON::Any))
if target = initial_data["twoColumnSearchResultsRenderer"]?
self.extract(target)
end
end
private def self.extract(target)
raw_items = [] of Array(JSON::Any)
target.dig("primaryContents", "sectionListRenderer", "contents").as_a.each do |node|
if node = node["itemSectionRenderer"]?
raw_items << node["contents"].as_a
end
end
return raw_items.flatten
end
end
# Extracts continuation items from a InnerTube response
#
# Continuation items (on YouTube) are items which are appended to the
# end of the page for continuous scrolling. As such, in many cases,
# the items are lacking information such as author or category title,
# since the original results has already rendered them on the top of the page.
#
# The way they are structured is too varied to be accurately written down here.
# However, they all eventually lead to an array of parsable items after traversing
# through the JSON structure.
module Continuation
def self.process(initial_data : Hash(String, JSON::Any))
if target = initial_data["continuationContents"]?
self.extract(target)
elsif target = initial_data["appendContinuationItemsAction"]?
self.extract(target)
end
end
private def self.extract(target)
raw_items = [] of JSON::Any
if content = target["gridContinuation"]?
raw_items = content["items"].as_a
elsif content = target["continuationItems"]?
raw_items = content.as_a
end
return raw_items
end
end
end
# Helper methods to aid in the parsing of InnerTube to data structs.
#
# Mostly used to extract out repeated structures to deal with code
# repetition.
private module HelperExtractors
# Retrieves the amount of videos present within the given InnerTube data.
#
# Returns a 0 when it's unable to do so
def self.get_video_count(container : JSON::Any) : Int32
if box = container["videoCountText"]?
return extract_text(box).try &.gsub(/\D/, "").to_i || 0
elsif box = container["videoCount"]?
return box.as_s.to_i
else
return 0
end
end
# Retrieve lowest quality thumbnail from InnerTube data
#
# TODO allow configuration of image quality (-1 is highest)
#
# Raises when it's unable to parse from the given JSON data.
def self.get_thumbnails(container : JSON::Any) : String
return container.dig("thumbnail", "thumbnails", 0, "url").as_s
end
# ditto
#
# YouTube sometimes sends the thumbnail as:
# {"thumbnails": [{"thumbnails": [{"url": "example.com"}, ...]}]}
def self.get_thumbnails_plural(container : JSON::Any) : String
return container.dig("thumbnails", 0, "thumbnails", 0, "url").as_s
end
# Retrieves the ID required for querying the InnerTube browse endpoint.
# Raises when it's unable to do so
def self.get_browse_id(container)
return container.dig("navigationEndpoint", "browseEndpoint", "browseId").as_s
end
end
# Extracts text from InnerTube response
#
# InnerTube can package text in three different formats
# "runs": [
# {"text": "something"},
# {"text": "cont"},
# ...
# ]
#
# "SimpleText": "something"
#
# Or sometimes just none at all as with the data returned from
# category continuations.
#
# In order to facilitate calling this function with `#[]?`:
# A nil will be accepted. Of course, since nil cannot be parsed,
# another nil will be returned.
def extract_text(item : JSON::Any?) : String?
if item.nil?
return nil
end
if text_container = item["simpleText"]?
return text_container.as_s
elsif text_container = item["runs"]?
return text_container.as_a.map(&.["text"].as_s).join("")
else
nil
end
end
# Parses an item from Youtube's JSON response into a more usable structure.
# The end result can either be a SearchVideo, SearchPlaylist or SearchChannel.
def extract_item(item : JSON::Any, author_fallback : String? = "",
author_id_fallback : String? = "")
# We "allow" nil values but secretly use empty strings instead. This is to save us the
# hassle of modifying every author_fallback and author_id_fallback arg usage
# which is more often than not nil.
author_fallback = AuthorFallback.new(author_fallback || "", author_id_fallback || "")
# Cycles through all of the item parsers and attempt to parse the raw YT JSON data.
# Each parser automatically validates the data given to see if the data is
# applicable to itself. If not nil is returned and the next parser is attemped.
ITEM_PARSERS.each do |parser|
if result = parser.process(item, author_fallback)
return result
end
end
end
# Parses multiple items from YouTube's initial JSON response into a more usable structure.
# The end result is an array of SearchItem.
def extract_items(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil,
author_id_fallback : String? = nil) : Array(SearchItem)
items = [] of SearchItem
if unpackaged_data = initial_data["contents"]?.try &.as_h
elsif unpackaged_data = initial_data["response"]?.try &.as_h
elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 0).try &.as_h
else
unpackaged_data = initial_data
end
# This is identical to the parser cycling of extract_item().
ITEM_CONTAINER_EXTRACTOR.each do |extractor|
if container = extractor.process(unpackaged_data)
# Extract items in container
container.each do |item|
if parsed_result = extract_item(item, author_fallback, author_id_fallback)
items << parsed_result
end
end
break
end
end
return items
end

View File

@ -42,15 +42,19 @@ struct ConfigPreferences
property player_style : String = "invidious"
property quality : String = "hd720"
property quality_dash : String = "auto"
property default_home : String = "Popular"
property default_home : String? = "Popular"
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
property automatic_instance_redirect : Bool = false
property related_videos : Bool = true
property sort : String = "published"
property speed : Float32 = 1.0_f32
property thin_mode : Bool = false
property unseen_only : Bool = false
property video_loop : Bool = false
property extend_desc : Bool = false
property volume : Int32 = 100
property vr_mode : Bool = true
property show_nick : Bool = true
def to_tuple
{% begin %}
@ -98,6 +102,7 @@ class Config
property port : Int32 = 3000 # Port to listen for connections (overrided by command line argument)
property host_binding : String = "0.0.0.0" # Host to bind (overrided by command line argument)
property pool_size : Int32 = 100 # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
property use_quic : Bool = true # Use quic transport for youtube api
@[YAML::Field(converter: Preferences::StringToCookies)]
property cookies : HTTP::Cookies = HTTP::Cookies.new # Saved cookies in "name1=value1; name2=value2..." format
@ -243,172 +248,40 @@ def html_to_content(description_html : String)
end
def extract_videos(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil)
extract_items(initial_data, author_fallback, author_id_fallback).select(&.is_a?(SearchVideo)).map(&.as(SearchVideo))
end
extracted = extract_items(initial_data, author_fallback, author_id_fallback)
def extract_item(item : JSON::Any, author_fallback : String? = nil, author_id_fallback : String? = nil)
if i = (item["videoRenderer"]? || item["gridVideoRenderer"]?)
video_id = i["videoId"].as_s
title = i["title"].try { |t| t["simpleText"]?.try &.as_s || t["runs"]?.try &.as_a.map(&.["text"].as_s).join("") } || ""
author_info = i["ownerText"]?.try &.["runs"]?.try &.as_a?.try &.[0]?
author = author_info.try &.["text"].as_s || author_fallback || ""
author_id = author_info.try &.["navigationEndpoint"]?.try &.["browseEndpoint"]["browseId"].as_s || author_id_fallback || ""
published = i["publishedTimeText"]?.try &.["simpleText"]?.try { |t| decode_date(t.as_s) } || Time.local
view_count = i["viewCountText"]?.try &.["simpleText"]?.try &.as_s.gsub(/\D+/, "").to_i64? || 0_i64
description_html = i["descriptionSnippet"]?.try { |t| parse_content(t) } || ""
length_seconds = i["lengthText"]?.try &.["simpleText"]?.try &.as_s.try { |t| decode_length_seconds(t) } ||
i["thumbnailOverlays"]?.try &.as_a.find(&.["thumbnailOverlayTimeStatusRenderer"]?).try &.["thumbnailOverlayTimeStatusRenderer"]?
.try &.["text"]?.try &.["simpleText"]?.try &.as_s.try { |t| decode_length_seconds(t) } || 0
live_now = false
paid = false
premium = false
premiere_timestamp = i["upcomingEventData"]?.try &.["startTime"]?.try { |t| Time.unix(t.as_s.to_i64) }
i["badges"]?.try &.as_a.each do |badge|
b = badge["metadataBadgeRenderer"]
case b["label"].as_s
when "LIVE NOW"
live_now = true
when "New", "4K", "CC"
# TODO
when "Premium"
paid = true
# TODO: Potentially available as i["topStandaloneBadge"]["metadataBadgeRenderer"]
premium = true
else nil # Ignore
end
target = [] of SearchItem
extracted.each do |i|
if i.is_a?(Category)
i.contents.each { |cate_i| target << cate_i if !cate_i.is_a? Video }
else
target << i
end
SearchVideo.new({
title: title,
id: video_id,
author: author,
ucid: author_id,
published: published,
views: view_count,
description_html: description_html,
length_seconds: length_seconds,
live_now: live_now,
paid: paid,
premium: premium,
premiere_timestamp: premiere_timestamp,
})
elsif i = item["channelRenderer"]?
author = i["title"]["simpleText"]?.try &.as_s || author_fallback || ""
author_id = i["channelId"]?.try &.as_s || author_id_fallback || ""
author_thumbnail = i["thumbnail"]["thumbnails"]?.try &.as_a[0]?.try &.["url"]?.try &.as_s || ""
subscriber_count = i["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s.try { |s| short_text_to_number(s.split(" ")[0]) } || 0
auto_generated = false
auto_generated = true if !i["videoCountText"]?
video_count = i["videoCountText"]?.try &.["runs"].as_a[0]?.try &.["text"].as_s.gsub(/\D/, "").to_i || 0
description_html = i["descriptionSnippet"]?.try { |t| parse_content(t) } || ""
SearchChannel.new({
author: author,
ucid: author_id,
author_thumbnail: author_thumbnail,
subscriber_count: subscriber_count,
video_count: video_count,
description_html: description_html,
auto_generated: auto_generated,
})
elsif i = item["gridPlaylistRenderer"]?
title = i["title"]["runs"].as_a[0]?.try &.["text"].as_s || ""
plid = i["playlistId"]?.try &.as_s || ""
video_count = i["videoCountText"]["runs"].as_a[0]?.try &.["text"].as_s.gsub(/\D/, "").to_i || 0
playlist_thumbnail = i["thumbnail"]["thumbnails"][0]?.try &.["url"]?.try &.as_s || ""
SearchPlaylist.new({
title: title,
id: plid,
author: author_fallback || "",
ucid: author_id_fallback || "",
video_count: video_count,
videos: [] of SearchPlaylistVideo,
thumbnail: playlist_thumbnail,
})
elsif i = item["playlistRenderer"]?
title = i["title"]["simpleText"]?.try &.as_s || ""
plid = i["playlistId"]?.try &.as_s || ""
video_count = i["videoCount"]?.try &.as_s.to_i || 0
playlist_thumbnail = i["thumbnails"].as_a[0]?.try &.["thumbnails"]?.try &.as_a[0]?.try &.["url"].as_s || ""
author_info = i["shortBylineText"]?.try &.["runs"]?.try &.as_a?.try &.[0]?
author = author_info.try &.["text"].as_s || author_fallback || ""
author_id = author_info.try &.["navigationEndpoint"]?.try &.["browseEndpoint"]["browseId"].as_s || author_id_fallback || ""
videos = i["videos"]?.try &.as_a.map do |v|
v = v["childVideoRenderer"]
v_title = v["title"]["simpleText"]?.try &.as_s || ""
v_id = v["videoId"]?.try &.as_s || ""
v_length_seconds = v["lengthText"]?.try &.["simpleText"]?.try { |t| decode_length_seconds(t.as_s) } || 0
SearchPlaylistVideo.new({
title: v_title,
id: v_id,
length_seconds: v_length_seconds,
})
end || [] of SearchPlaylistVideo
# TODO: i["publishedTimeText"]?
SearchPlaylist.new({
title: title,
id: plid,
author: author,
ucid: author_id,
video_count: video_count,
videos: videos,
thumbnail: playlist_thumbnail,
})
elsif i = item["radioRenderer"]? # Mix
# TODO
elsif i = item["showRenderer"]? # Show
# TODO
elsif i = item["shelfRenderer"]?
elsif i = item["horizontalCardListRenderer"]?
elsif i = item["searchPyvRenderer"]? # Ad
end
return target.select(&.is_a?(SearchVideo)).map(&.as(SearchVideo))
end
def extract_items(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil)
items = [] of SearchItem
def extract_selected_tab(tabs)
# Extract the selected tab from the array of tabs Youtube returns
return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"].as_bool)[0]["tabRenderer"]
end
channel_v2_response = initial_data
.try &.["continuationContents"]?
.try &.["gridContinuation"]?
.try &.["items"]?
def fetch_continuation_token(items : Array(JSON::Any))
# Fetches the continuation token from an array of items
return items.last["continuationItemRenderer"]?
.try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s
end
if channel_v2_response
channel_v2_response.try &.as_a.each { |item|
extract_item(item, author_fallback, author_id_fallback)
.try { |t| items << t }
}
def fetch_continuation_token(initial_data : Hash(String, JSON::Any))
# Fetches the continuation token from initial data
if initial_data["onResponseReceivedActions"]?
continuation_items = initial_data["onResponseReceivedActions"][0]["appendContinuationItemsAction"]["continuationItems"]
else
initial_data.try { |t| t["contents"]? || t["response"]? }
.try { |t| t["twoColumnBrowseResultsRenderer"]?.try &.["tabs"].as_a.select(&.["tabRenderer"]?.try &.["selected"].as_bool)[0]?.try &.["tabRenderer"]["content"] ||
t["twoColumnSearchResultsRenderer"]?.try &.["primaryContents"] ||
t["continuationContents"]? }
.try { |t| t["sectionListRenderer"]? || t["sectionListContinuation"]? }
.try &.["contents"].as_a
.each { |c| c.try &.["itemSectionRenderer"]?.try &.["contents"].as_a
.try { |t| t[0]?.try &.["shelfRenderer"]?.try &.["content"]["expandedShelfContentsRenderer"]?.try &.["items"].as_a ||
t[0]?.try &.["gridRenderer"]?.try &.["items"].as_a || t }
.each { |item|
extract_item(item, author_fallback, author_id_fallback)
.try { |t| items << t }
} }
tab = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"])
continuation_items = tab["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]["contents"][0]["gridRenderer"]["items"]
end
items
return fetch_continuation_token(continuation_items.as_a)
end
def check_enum(db, enum_name, struct_type = nil)
@ -504,12 +377,6 @@ def check_table(db, table_name, struct_type = nil)
end
end
class PG::ResultSet
def field(index = @column_index)
@fields.not_nil![index]
end
end
def get_column_array(db, table_name)
column_array = [] of String
db.query("SELECT * FROM #{table_name} LIMIT 0") do |rs|
@ -678,7 +545,7 @@ def create_notification_stream(env, topics, connection_channel)
end
def extract_initial_data(body) : Hash(String, JSON::Any)
return JSON.parse(body.match(/(window\["ytInitialData"\]|var\s*ytInitialData)\s*=\s*(?<info>\{.*?\});/mx).try &.["info"] || "{}").as_h
return JSON.parse(body.match(/(window\["ytInitialData"\]|var\s*ytInitialData)\s*=\s*(?<info>{.*?});<\/script>/mx).try &.["info"] || "{}").as_h
end
def proxy_file(response, env)
@ -694,85 +561,3 @@ def proxy_file(response, env)
IO.copy response.body_io, env.response
end
end
# See https://github.com/kemalcr/kemal/pull/576
class HTTP::Server::Response::Output
def close
return if closed?
unless response.wrote_headers?
response.content_length = @out_count
end
ensure_headers_written
super
if @chunked
@io << "0\r\n\r\n"
@io.flush
end
end
end
class HTTP::Client::Response
def pipe(io)
HTTP.serialize_body(io, headers, @body, @body_io, @version)
end
end
# Supports serialize_body without first writing headers
module HTTP
def self.serialize_body(io, headers, body, body_io, version)
if body
io << body
elsif body_io
content_length = content_length(headers)
if content_length
copied = IO.copy(body_io, io)
if copied != content_length
raise ArgumentError.new("Content-Length header is #{content_length} but body had #{copied} bytes")
end
elsif Client::Response.supports_chunked?(version)
headers["Transfer-Encoding"] = "chunked"
serialize_chunked_body(io, body_io)
else
io << body
end
end
end
end
class HTTP::Client
property family : Socket::Family = Socket::Family::UNSPEC
private def socket
socket = @socket
return socket if socket
hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host
socket = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout, @family
socket.read_timeout = @read_timeout if @read_timeout
socket.sync = false
{% if !flag?(:without_openssl) %}
if tls = @tls
socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: @host)
end
{% end %}
@socket = socket
end
end
class TCPSocket
def initialize(host, port, dns_timeout = nil, connect_timeout = nil, family = Socket::Family::UNSPEC)
Addrinfo.tcp(host, port, timeout: dns_timeout, family: family) do |addrinfo|
super(addrinfo.family, addrinfo.type, addrinfo.protocol)
connect(addrinfo, timeout: connect_timeout) do |error|
close
error
end
end
end
end

View File

@ -1,3 +1,46 @@
# "bn_BD" => load_locale("bn_BD"), # Bengali (Bangladesh) [Incomplete]
# "eu" => load_locale("eu"), # Basque [Incomplete]
# "si" => load_locale("si"), # Sinhala [Incomplete]
# "sk" => load_locale("sk"), # Slovak [Incomplete]
# "sr" => load_locale("sr"), # Serbian [Incomplete]
# "sr_Cyrl" => load_locale("sr_Cyrl"), # Serbian (cyrillic) [Incomplete]
LOCALES = {
"ar" => load_locale("ar"), # Arabic
"cs" => load_locale("cs"), # Czech
"da" => load_locale("da"), # Danish
"de" => load_locale("de"), # German
"el" => load_locale("el"), # Greek
"en-US" => load_locale("en-US"), # English (US)
"eo" => load_locale("eo"), # Esperanto
"es" => load_locale("es"), # Spanish
"fa" => load_locale("fa"), # Persian
"fi" => load_locale("fi"), # Finnish
"fr" => load_locale("fr"), # French
"he" => load_locale("he"), # Hebrew
"hr" => load_locale("hr"), # Croatian
"hu-HU" => load_locale("hu-HU"), # Hungarian
"id" => load_locale("id"), # Indonesian
"is" => load_locale("is"), # Icelandic
"it" => load_locale("it"), # Italian
"ja" => load_locale("ja"), # Japanese
"ko" => load_locale("ko"), # Korean
"lt" => load_locale("lt"), # Lithuanian
"nb-NO" => load_locale("nb-NO"), # Norwegian Bokmål
"nl" => load_locale("nl"), # Dutch
"pl" => load_locale("pl"), # Polish
"pt" => load_locale("pt"), # Portuguese
"pt-BR" => load_locale("pt-BR"), # Portuguese (Brazil)
"pt-PT" => load_locale("pt-PT"), # Portuguese (Portugal)
"ro" => load_locale("ro"), # Romanian
"ru" => load_locale("ru"), # Russian
"sv-SE" => load_locale("sv-SE"), # Swedish
"tr" => load_locale("tr"), # Turkish
"uk" => load_locale("uk"), # Ukrainian
"vi" => load_locale("vi"), # Vietnamese
"zh-CN" => load_locale("zh-CN"), # Chinese (Simplified)
"zh-TW" => load_locale("zh-TW"), # Chinese (Traditional)
}
def load_locale(name)
return JSON.parse(File.read("locales/#{name}.json")).as_h
end

View File

@ -17,7 +17,19 @@ class Invidious::LogHandler < Kemal::BaseLogHandler
elapsed_time = Time.measure { call_next(context) }
elapsed_text = elapsed_text(elapsed_time)
info("#{context.response.status_code} #{context.request.method} #{context.request.resource} #{elapsed_text}")
# Default: full path with parameters
requested_url = context.request.resource
# Try not to log search queries passed as GET parameters during normal use
# (They will still be logged if log level is 'Debug' or 'Trace')
if @level > LogLevel::Debug && (
requested_url.downcase.includes?("search") || requested_url.downcase.includes?("q=")
)
# Log only the path
requested_url = context.request.path
end
info("#{context.response.status_code} #{context.request.method} #{requested_url} #{elapsed_text}")
context
end

View File

@ -48,10 +48,20 @@ module JSON::Serializable
end
end
macro templated(filename, template = "template")
macro templated(filename, template = "template", navbar_search = true)
navbar_search = {{navbar_search}}
render "src/invidious/views/#{{{filename}}}.ecr", "src/invidious/views/#{{{template}}}.ecr"
end
macro rendered(filename)
render "src/invidious/views/#{{{filename}}}.ecr"
end
# Similar to Kemals halt method but works in a
# method.
macro haltf(env, status_code = 200, response = "")
{{env}}.response.status_code = {{status_code}}
{{env}}.response.print {{response}}
{{env}}.response.close
return
end

View File

@ -0,0 +1,256 @@
struct SearchVideo
include DB::Serializable
property title : String
property id : String
property author : String
property ucid : String
property published : Time
property views : Int64
property description_html : String
property length_seconds : Int32
property live_now : Bool
property premium : Bool
property premiere_timestamp : Time?
def to_xml(auto_generated, query_params, xml : XML::Builder)
query_params["v"] = self.id
xml.element("entry") do
xml.element("id") { xml.text "yt:video:#{self.id}" }
xml.element("yt:videoId") { xml.text self.id }
xml.element("yt:channelId") { xml.text self.ucid }
xml.element("title") { xml.text self.title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}")
xml.element("author") do
if auto_generated
xml.element("name") { xml.text self.author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" }
else
xml.element("name") { xml.text author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
end
end
xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg")
end
xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text html_to_content(self.description_html) }
end
end
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("media:group") do
xml.element("media:title") { xml.text self.title }
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg",
width: "320", height: "180")
xml.element("media:description") { xml.text html_to_content(self.description_html) }
end
xml.element("media:community") do
xml.element("media:statistics", views: self.views)
end
end
end
def to_xml(auto_generated, query_params, xml : XML::Builder | Nil = nil)
if xml
to_xml(HOST_URL, auto_generated, query_params, xml)
else
XML.build do |json|
to_xml(HOST_URL, auto_generated, query_params, xml)
end
end
end
def to_json(locale : Hash(String, JSON::Any), json : JSON::Builder)
json.object do
json.field "type", "video"
json.field "title", self.title
json.field "videoId", self.id
json.field "author", self.author
json.field "authorId", self.ucid
json.field "authorUrl", "/channel/#{self.ucid}"
json.field "videoThumbnails" do
generate_thumbnails(json, self.id)
end
json.field "description", html_to_content(self.description_html)
json.field "descriptionHtml", self.description_html
json.field "viewCount", self.views
json.field "published", self.published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "lengthSeconds", self.length_seconds
json.field "liveNow", self.live_now
json.field "premium", self.premium
json.field "isUpcoming", self.is_upcoming
if self.premiere_timestamp
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
end
end
end
def to_json(locale, json : JSON::Builder | Nil = nil)
if json
to_json(locale, json)
else
JSON.build do |json|
to_json(locale, json)
end
end
end
def is_upcoming
premiere_timestamp ? true : false
end
end
struct SearchPlaylistVideo
include DB::Serializable
property title : String
property id : String
property length_seconds : Int32
end
struct SearchPlaylist
include DB::Serializable
property title : String
property id : String
property author : String
property ucid : String
property video_count : Int32
property videos : Array(SearchPlaylistVideo)
property thumbnail : String?
def to_json(locale, json : JSON::Builder)
json.object do
json.field "type", "playlist"
json.field "title", self.title
json.field "playlistId", self.id
json.field "playlistThumbnail", self.thumbnail
json.field "author", self.author
json.field "authorId", self.ucid
json.field "authorUrl", "/channel/#{self.ucid}"
json.field "videoCount", self.video_count
json.field "videos" do
json.array do
self.videos.each do |video|
json.object do
json.field "title", video.title
json.field "videoId", video.id
json.field "lengthSeconds", video.length_seconds
json.field "videoThumbnails" do
generate_thumbnails(json, video.id)
end
end
end
end
end
end
end
def to_json(locale, json : JSON::Builder | Nil = nil)
if json
to_json(locale, json)
else
JSON.build do |json|
to_json(locale, json)
end
end
end
end
struct SearchChannel
include DB::Serializable
property author : String
property ucid : String
property author_thumbnail : String
property subscriber_count : Int32
property video_count : Int32
property description_html : String
property auto_generated : Bool
def to_json(locale, json : JSON::Builder)
json.object do
json.field "type", "channel"
json.field "author", self.author
json.field "authorId", self.ucid
json.field "authorUrl", "/channel/#{self.ucid}"
json.field "authorThumbnails" do
json.array do
qualities = {32, 48, 76, 100, 176, 512}
qualities.each do |quality|
json.object do
json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", quality
end
end
end
end
json.field "autoGenerated", self.auto_generated
json.field "subCount", self.subscriber_count
json.field "videoCount", self.video_count
json.field "description", html_to_content(self.description_html)
json.field "descriptionHtml", self.description_html
end
end
def to_json(locale, json : JSON::Builder | Nil = nil)
if json
to_json(locale, json)
else
JSON.build do |json|
to_json(locale, json)
end
end
end
end
class Category
include DB::Serializable
property title : String
property contents : Array(SearchItem) | Array(Video)
property url : String?
property description_html : String
property badges : Array(Tuple(String, String))?
def to_json(locale, json : JSON::Builder)
json.object do
json.field "title", self.title
json.field "contents", self.contents
end
end
def to_json(locale, json : JSON::Builder | Nil = nil)
if json
to_json(locale, json)
else
JSON.build do |json|
to_json(locale, json)
end
end
end
end
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | Category

View File

@ -1,5 +1,5 @@
require "lsquic"
require "pool/connection"
require "db"
def add_yt_headers(request)
request.headers["user-agent"] ||= "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36"
@ -9,20 +9,22 @@ def add_yt_headers(request)
return if request.resource.starts_with? "/sorry/index"
request.headers["x-youtube-client-name"] ||= "1"
request.headers["x-youtube-client-version"] ||= "2.20200609"
# Preserve original cookies and add new YT consent cookie for EU servers
request.headers["cookie"] = "#{request.headers["cookie"]?}; CONSENT=YES+"
if !CONFIG.cookies.empty?
request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
end
end
struct QUICPool
struct YoutubeConnectionPool
property! url : URI
property! capacity : Int32
property! timeout : Float64
property pool : ConnectionPool(QUIC::Client)
property pool : DB::Pool(QUIC::Client | HTTP::Client)
def initialize(url : URI, @capacity = 5, @timeout = 5.0)
def initialize(url : URI, @capacity = 5, @timeout = 5.0, use_quic = true)
@url = url
@pool = build_pool
@pool = build_pool(use_quic)
end
def client(region = nil, &block)
@ -41,16 +43,20 @@ struct QUICPool
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
response = yield conn
ensure
pool.checkin(conn)
pool.release(conn)
end
end
response
end
private def build_pool
ConnectionPool(QUIC::Client).new(capacity: capacity, timeout: timeout) do
conn = QUIC::Client.new(url)
private def build_pool(use_quic)
DB::Pool(QUIC::Client | HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
if use_quic
conn = QUIC::Client.new(url)
else
conn = HTTP::Client.new(url)
end
conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
@ -292,7 +298,7 @@ def make_host_url(kemal_config)
# Add if non-standard port
if port != 80 && port != 443
port = ":#{kemal_config.port}"
port = ":#{port}"
else
port = ""
end
@ -403,3 +409,65 @@ def convert_theme(theme)
theme
end
end
def fetch_random_instance
begin
instance_api_client = make_client(URI.parse("https://api.invidious.io"))
# Timeouts
instance_api_client.connect_timeout = 10.seconds
instance_api_client.dns_timeout = 10.seconds
instance_list = JSON.parse(instance_api_client.get("/instances.json").body).as_a
instance_api_client.close
rescue Socket::ConnectError | IO::TimeoutError | JSON::ParseException
instance_list = [] of JSON::Any
end
filtered_instance_list = [] of String
instance_list.each do |data|
# TODO Check if current URL is onion instance and use .onion types if so.
if data[1]["type"] == "https"
# Instances can have statisitics disabled, which is an requirement of version validation.
# as_nil? doesn't exist. Thus we'll have to handle the error rasied if as_nil fails.
begin
data[1]["stats"].as_nil
next
rescue TypeCastError
end
# stats endpoint could also lack the software dict.
next if data[1]["stats"]["software"]?.nil?
# Makes sure the instance isn't too outdated.
if remote_version = data[1]["stats"]?.try &.["software"]?.try &.["version"]
remote_commit_date = remote_version.as_s.match(/\d{4}\.\d{2}\.\d{2}/)
next if !remote_commit_date
remote_commit_date = Time.parse(remote_commit_date[0], "%Y.%m.%d", Time::Location::UTC)
local_commit_date = Time.parse(CURRENT_VERSION, "%Y.%m.%d", Time::Location::UTC)
next if (remote_commit_date - local_commit_date).abs.days > 30
begin
data[1]["monitor"].as_nil
health = data[1]["monitor"].as_h["dailyRatios"][0].as_h["ratio"]
filtered_instance_list << data[0].as_s if health.to_s.to_f > 90
rescue TypeCastError
# We can't check the health if the monitoring is broken. Thus we'll just add it to the list
# and move on. Ideally we'll ignore any instance that has broken health monitoring but due to the fact that
# it's an error that often occurs with all the instances at the same time, we have to just skip the check.
filtered_instance_list << data[0].as_s
end
end
end
end
# If for some reason no instances managed to get fetched successfully then we'll just redirect to redirect.invidious.io
if filtered_instance_list.size == 0
return "redirect.invidious.io"
end
return filtered_instance_list.sample(1)[0]
end

View File

@ -0,0 +1,447 @@
#
# This file contains youtube API wrappers
#
module YoutubeAPI
extend self
# Enumerate used to select one of the clients supported by the API
enum ClientType
Web
WebEmbeddedPlayer
WebMobile
WebScreenEmbed
Android
AndroidEmbeddedPlayer
AndroidScreenEmbed
end
# List of hard-coded values used by the different clients
HARDCODED_CLIENTS = {
ClientType::Web => {
name: "WEB",
version: "2.20210721.00.00",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
screen: "WATCH_FULL_SCREEN",
},
ClientType::WebEmbeddedPlayer => {
name: "WEB_EMBEDDED_PLAYER", # 56
version: "1.20210721.1.0",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
screen: "EMBED",
},
ClientType::WebMobile => {
name: "MWEB",
version: "2.20210726.08.00",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
screen: "", # None
},
ClientType::WebScreenEmbed => {
name: "WEB",
version: "2.20210721.00.00",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
screen: "EMBED",
},
ClientType::Android => {
name: "ANDROID",
version: "16.20",
api_key: "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w",
screen: "", # ??
},
ClientType::AndroidEmbeddedPlayer => {
name: "ANDROID_EMBEDDED_PLAYER", # 55
version: "16.20",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
screen: "", # None?
},
ClientType::AndroidScreenEmbed => {
name: "ANDROID", # 3
version: "16.20",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
screen: "EMBED",
},
}
####################################################################
# struct ClientConfig
#
# Data structure used to pass a client configuration to the different
# API endpoints handlers.
#
# Use case examples:
#
# ```
# # Get Norwegian search results
# conf_1 = ClientConfig.new(region: "NO")
# YoutubeAPI::search("Kollektivet", params: "", client_config: conf_1)
#
# # Use the Android client to request video streams URLs
# conf_2 = ClientConfig.new(client_type: ClientType::Android)
# YoutubeAPI::player(video_id: "dQw4w9WgXcQ", client_config: conf_2)
#
# # Proxy request through russian proxies
# conf_3 = ClientConfig.new(proxy_region: "RU")
# YoutubeAPI::next({video_id: "dQw4w9WgXcQ"}, client_config: conf_3)
# ```
#
struct ClientConfig
# Type of client to emulate.
# See `enum ClientType` and `HARDCODED_CLIENTS`.
property client_type : ClientType
# Region to provide to youtube, e.g to alter search results
# (this is passed as the `gl` parmeter).
property region : String | Nil
# ISO code of country where the proxy is located.
# Used in case of geo-restricted videos.
property proxy_region : String | Nil
# Initialization function
def initialize(
*,
@client_type = ClientType::Web,
@region = "US",
@proxy_region = nil
)
end
# Getter functions that provides easy access to hardcoded clients
# parameters (name/version strings and related API key)
def name : String
HARDCODED_CLIENTS[@client_type][:name]
end
# :ditto:
def version : String
HARDCODED_CLIENTS[@client_type][:version]
end
# :ditto:
def api_key : String
HARDCODED_CLIENTS[@client_type][:api_key]
end
# :ditto:
def screen : String
HARDCODED_CLIENTS[@client_type][:screen]
end
# Convert to string, for logging purposes
def to_s
return {
client_type: self.name,
region: @region,
proxy_region: @proxy_region,
}.to_s
end
end
# Default client config, used if nothing is passed
DEFAULT_CLIENT_CONFIG = ClientConfig.new
####################################################################
# make_context(client_config)
#
# Return, as a Hash, the "context" data required to request the
# youtube API endpoints.
#
private def make_context(client_config : ClientConfig | Nil) : Hash
# Use the default client config if nil is passed
client_config ||= DEFAULT_CLIENT_CONFIG
client_context = {
"client" => {
"hl" => "en",
"gl" => client_config.region || "US", # Can't be empty!
"clientName" => client_config.name,
"clientVersion" => client_config.version,
},
}
# Add some more context if it exists in the client definitions
if !client_config.screen.empty?
client_context["client"]["clientScreen"] = client_config.screen
end
if client_config.screen == "EMBED"
client_context["thirdParty"] = {
"embedUrl" => "https://www.youtube.com/embed/dQw4w9WgXcQ",
}
end
return client_context
end
####################################################################
# browse(continuation, client_config?)
# browse(browse_id, params, client_config?)
#
# Requests the youtubei/v1/browse endpoint with the required headers
# and POST data in order to get a JSON reply in english that can
# be easily parsed.
#
# Both forms can take an optional ClientConfig parameter (see
# `struct ClientConfig` above for more details).
#
# The requested data can either be:
#
# - A continuation token (ctoken). Depending on this token's
# contents, the returned data can be playlist videos, channel
# community tab content, channel info, ...
#
# - A playlist ID (parameters MUST be an empty string)
#
def browse(continuation : String, client_config : ClientConfig | Nil = nil)
# JSON Request data, required by the API
data = {
"context" => self.make_context(client_config),
"continuation" => continuation,
}
return self._post_json("/youtubei/v1/browse", data, client_config)
end
# :ditto:
def browse(
browse_id : String,
*, # Force the following paramters to be passed by name
params : String,
client_config : ClientConfig | Nil = nil
)
# JSON Request data, required by the API
data = {
"browseId" => browse_id,
"context" => self.make_context(client_config),
}
# Append the additionnal parameters if those were provided
# (this is required for channel info, playlist and community, e.g)
if params != ""
data["params"] = params
end
return self._post_json("/youtubei/v1/browse", data, client_config)
end
####################################################################
# next(continuation, client_config?)
# next(data, client_config?)
#
# Requests the youtubei/v1/next endpoint with the required headers
# and POST data in order to get a JSON reply in english that can
# be easily parsed.
#
# Both forms can take an optional ClientConfig parameter (see
# `struct ClientConfig` above for more details).
#
# The requested data can be:
#
# - A continuation token (ctoken). Depending on this token's
# contents, the returned data can be videos comments,
# their replies, ... In this case, the string must be passed
# directly to the function. E.g:
#
# ```
# YoutubeAPI::next("ABCDEFGH_abcdefgh==")
# ```
#
# - Arbitrary parameters, in Hash form. See examples below for
# known examples of arbitrary data that can be passed to YouTube:
#
# ```
# # Get the videos related to a specific video ID
# YoutubeAPI::next({"videoId" => "dQw4w9WgXcQ"})
#
# # Get a playlist video's details
# YoutubeAPI::next({
# "videoId" => "9bZkp7q19f0",
# "playlistId" => "PL_oFlvgqkrjUVQwiiE3F3k3voF4tjXeP0",
# })
# ```
#
def next(continuation : String, *, client_config : ClientConfig | Nil = nil)
# JSON Request data, required by the API
data = {
"context" => self.make_context(client_config),
"continuation" => continuation,
}
return self._post_json("/youtubei/v1/next", data, client_config)
end
# :ditto:
def next(data : Hash, *, client_config : ClientConfig | Nil = nil)
# JSON Request data, required by the API
data2 = data.merge({
"context" => self.make_context(client_config),
})
return self._post_json("/youtubei/v1/next", data2, client_config)
end
# Allow a NamedTuple to be passed, too.
def next(data : NamedTuple, *, client_config : ClientConfig | Nil = nil)
return self.next(data.to_h, client_config: client_config)
end
####################################################################
# player(video_id, params, client_config?)
#
# Requests the youtubei/v1/player endpoint with the required headers
# and POST data in order to get a JSON reply.
#
# The requested data is a video ID (`v=` parameter), with some
# additional paramters, formatted as a base64 string.
#
# An optional ClientConfig parameter can be passed, too (see
# `struct ClientConfig` above for more details).
#
def player(
video_id : String,
*, # Force the following paramters to be passed by name
params : String,
client_config : ClientConfig | Nil = nil
)
# JSON Request data, required by the API
data = {
"videoId" => video_id,
"context" => self.make_context(client_config),
}
# Append the additionnal parameters if those were provided
if params != ""
data["params"] = params
end
return self._post_json("/youtubei/v1/player", data, client_config)
end
####################################################################
# resolve_url(url, client_config?)
#
# Requests the youtubei/v1/navigation/resolve_url endpoint with the
# required headers and POST data in order to get a JSON reply.
#
# An optional ClientConfig parameter can be passed, too (see
# `struct ClientConfig` above for more details).
#
# Output:
#
# ```
# # Valid channel "brand URL" gives the related UCID and browse ID
# channel_a = YoutubeAPI.resolve_url("https://youtube.com/c/google")
# channel_a # => {
# "endpoint": {
# "browseEndpoint": {
# "params": "EgC4AQA%3D",
# "browseId":"UCK8sQmJBp8GCxrOtXWBpyEA"
# },
# ...
# }
# }
#
# # Invalid URL returns throws an InfoException
# channel_b = YoutubeAPI.resolve_url("https://youtube.com/c/invalid")
# ```
#
def resolve_url(url : String, client_config : ClientConfig | Nil = nil)
data = {
"context" => self.make_context(nil),
"url" => url,
}
return self._post_json("/youtubei/v1/navigation/resolve_url", data, client_config)
end
####################################################################
# search(search_query, params, client_config?)
#
# Requests the youtubei/v1/search endpoint with the required headers
# and POST data in order to get a JSON reply. As the search results
# vary depending on the region, a region code can be specified in
# order to get non-US results.
#
# The requested data is a search string, with some additional
# paramters, formatted as a base64 string.
#
# An optional ClientConfig parameter can be passed, too (see
# `struct ClientConfig` above for more details).
#
def search(
search_query : String,
params : String,
client_config : ClientConfig | Nil = nil
)
# JSON Request data, required by the API
data = {
"query" => search_query,
"context" => self.make_context(client_config),
"params" => params,
}
return self._post_json("/youtubei/v1/search", data, client_config)
end
####################################################################
# _post_json(endpoint, data, client_config?)
#
# Internal function that does the actual request to youtube servers
# and handles errors.
#
# The requested data is an endpoint (URL without the domain part)
# and the data as a Hash object.
#
def _post_json(
endpoint : String,
data : Hash,
client_config : ClientConfig | Nil
) : Hash(String, JSON::Any)
# Use the default client config if nil is passed
client_config ||= DEFAULT_CLIENT_CONFIG
# Query parameters
url = "#{endpoint}?key=#{client_config.api_key}"
headers = HTTP::Headers{
"Content-Type" => "application/json; charset=UTF-8",
"Accept-Encoding" => "gzip",
}
# Logging
LOGGER.debug("YoutubeAPI: Using endpoint: \"#{endpoint}\"")
LOGGER.trace("YoutubeAPI: ClientConfig: #{client_config.to_s}")
LOGGER.trace("YoutubeAPI: POST data: #{data.to_s}")
# Send the POST request
if client_config.proxy_region
response = YT_POOL.client(
client_config.proxy_region,
&.post(url, headers: headers, body: data.to_json)
)
else
response = YT_POOL.client &.post(
url, headers: headers, body: data.to_json
)
end
# Convert result to Hash
initial_data = JSON.parse(response.body).as_h
# Error handling
if initial_data.has_key?("error")
code = initial_data["error"]["code"]
message = initial_data["error"]["message"].to_s.sub(/(\\n)+\^$/, "")
# Logging
LOGGER.error("YoutubeAPI: Got error #{code} when requesting #{endpoint}")
LOGGER.error("YoutubeAPI: #{message}")
LOGGER.info("YoutubeAPI: POST data was: #{data.to_s}")
raise InfoException.new("Could not extract JSON. Youtube API returned \
error #{code} with message:<br>\"#{message}\"")
end
return initial_data
end
end # End of module

Some files were not shown because too many files have changed in this diff Show More