Merge branch 'master' into optional-disable-api-features

This commit is contained in:
Richard Lora 2026-05-10 19:36:20 -04:00 committed by GitHub
commit ed87890e85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 799 additions and 838 deletions

View File

@ -29,7 +29,7 @@ jobs:
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
platform: linux/arm64/v8 platform: linux/arm64/v8
name: "ARM64" name: "ARM64"
dockerfile: "docker/Dockerfile.arm64" dockerfile: "docker/Dockerfile"
tag_suffix: "-arm64" tag_suffix: "-arm64"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -39,10 +39,10 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Login to registry - name: Login to registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
@ -50,7 +50,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: quay.io/invidious/invidious images: quay.io/invidious/invidious
flavor: | flavor: |
@ -62,13 +62,34 @@ jobs:
quay.expires-after=12w quay.expires-after=12w
- name: Build and push Docker ${{ matrix.name }} image for Push Event - name: Build and push Docker ${{ matrix.name }} image for Push Event
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: ${{ matrix.dockerfile }} file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
build-args: | build-args: |
"release=1" "release=1"
combine-multiarch-images:
needs: release
runs-on: ubuntu-latest
steps:
- name: Login to registry
uses: docker/login-action@v4
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
# https://github.com/marketplace/actions/docker-manifest-create-action
- name: Create and push manifest
uses: int128/docker-manifest-create-action@v2.20.0
with:
push: true
tags: quay.io/invidious/invidious:master
sources: |
quay.io/invidious/invidious:master
quay.io/invidious/invidious:master-arm64

View File

@ -20,7 +20,7 @@ jobs:
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
platform: linux/arm64/v8 platform: linux/arm64/v8
name: "ARM64" name: "ARM64"
dockerfile: "docker/Dockerfile.arm64" dockerfile: "docker/Dockerfile"
tag_suffix: "-arm64" tag_suffix: "-arm64"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -30,10 +30,10 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Login to registry - name: Login to registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
@ -41,7 +41,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: quay.io/invidious/invidious images: quay.io/invidious/invidious
flavor: | flavor: |
@ -54,13 +54,34 @@ jobs:
quay.expires-after=12w quay.expires-after=12w
- name: Build and push Docker ${{ matrix.name }} image for Push Event - name: Build and push Docker ${{ matrix.name }} image for Push Event
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: ${{ matrix.dockerfile }} file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
build-args: | build-args: |
"release=1" "release=1"
combine-multiarch-images:
needs: release
runs-on: ubuntu-latest
steps:
- name: Login to registry
uses: docker/login-action@v4
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
# https://github.com/marketplace/actions/docker-manifest-create-action
- name: Create and push manifest
uses: int128/docker-manifest-create-action@v2.20.0
with:
push: true
tags: quay.io/invidious/invidious:latest
sources: |
quay.io/invidious/invidious:latest
quay.io/invidious/invidious:latest-arm64

View File

@ -43,6 +43,8 @@ jobs:
- 1.16.3 - 1.16.3
- 1.17.1 - 1.17.1
- 1.18.2 - 1.18.2
- 1.19.2
- 1.20.1
include: include:
- crystal: nightly - crystal: nightly
stable: false stable: false
@ -58,7 +60,7 @@ jobs:
shell: bash shell: bash
- name: Install Crystal - name: Install Crystal
uses: crystal-lang/install-crystal@v1.9.1 uses: crystal-lang/install-crystal@v1.9.2
with: with:
crystal: ${{ matrix.crystal }} crystal: ${{ matrix.crystal }}
@ -80,7 +82,7 @@ jobs:
run: crystal spec run: crystal spec
- name: Build - name: Build
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr run: crystal build --warnings all --error-on-warnings --stats --time --progress --error-trace src/invidious.cr
build-docker: build-docker:
strategy: strategy:
@ -98,10 +100,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- name: Use ARM64 Dockerfile if ARM64
if: ${{ matrix.name == 'ARM64' }}
run: sed -i 's/Dockerfile/Dockerfile.arm64/' docker-compose.yml
- name: Build Docker - name: Build Docker
run: docker compose build run: docker compose build
@ -134,7 +132,7 @@ jobs:
- name: Install Crystal - name: Install Crystal
id: lint_step_install_crystal id: lint_step_install_crystal
uses: crystal-lang/install-crystal@v1.9.1 uses: crystal-lang/install-crystal@v1.9.2
with: with:
crystal: latest crystal: latest

View File

@ -104,14 +104,15 @@ if (video_data.params.quality === 'dash') {
* *
* @param {String} url * @param {String} url
* @param {String} [base] * @param {String} [base]
* @param {'t' | 'start'} param
* @returns {URL} urlWithTimeArg * @returns {URL} urlWithTimeArg
*/ */
function addCurrentTimeToURL(url, base) { function addCurrentTimeToURL(url, base, param = 't') {
var urlUsed = new URL(url, base); var urlUsed = new URL(url, base);
urlUsed.searchParams.delete('start'); urlUsed.searchParams.delete('start');
var currentTime = Math.ceil(player.currentTime()); var currentTime = Math.ceil(player.currentTime());
if (currentTime > 0) if (currentTime > 0)
urlUsed.searchParams.set('t', currentTime); urlUsed.searchParams.set(param, currentTime);
else if (urlUsed.searchParams.has('t')) else if (urlUsed.searchParams.has('t'))
urlUsed.searchParams.delete('t'); urlUsed.searchParams.delete('t');
return urlUsed; return urlUsed;
@ -147,7 +148,7 @@ player.on('timeupdate', function () {
let elem_yt_embed = document.getElementById('link-yt-embed'); let elem_yt_embed = document.getElementById('link-yt-embed');
if (elem_yt_embed) { if (elem_yt_embed) {
let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url'); let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url');
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed); elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed, undefined, 'start');
} }
} }

View File

@ -2,7 +2,7 @@
ARG OPENSSL_VERSION='3.5.2' ARG OPENSSL_VERSION='3.5.2'
ARG OPENSSL_SHA256='c53a47e5e441c930c3928cf7bf6fb00e5d129b630e0aa873b08258656e7345ec' ARG OPENSSL_SHA256='c53a47e5e441c930c3928cf7bf6fb00e5d129b630e0aa873b08258656e7345ec'
FROM crystallang/crystal:1.16.3-alpine AS dependabot-crystal FROM crystallang/crystal:1.20.1-alpine AS dependabot-crystal
# We compile openssl ourselves due to a memory leak in how crystal interacts # We compile openssl ourselves due to a memory leak in how crystal interacts
# with openssl # with openssl

View File

@ -1,83 +0,0 @@
# https://github.com/openssl/openssl/releases/tag/openssl-3.5.2
ARG OPENSSL_VERSION='3.5.2'
ARG OPENSSL_SHA256='c53a47e5e441c930c3928cf7bf6fb00e5d129b630e0aa873b08258656e7345ec'
FROM alpine:3.22 AS dependabot-alpine
# We compile openssl ourselves due to a memory leak in how crystal interacts
# with openssl
# Reference: https://github.com/iv-org/invidious/issues/1438#issuecomment-3087636228
FROM dependabot-alpine AS openssl-builder
RUN apk add --no-cache curl perl linux-headers build-base
WORKDIR /
ARG OPENSSL_VERSION
ARG OPENSSL_SHA256
RUN curl -Ls "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" --output openssl-${OPENSSL_VERSION}.tar.gz
RUN echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c
RUN tar -xzvf openssl-${OPENSSL_VERSION}.tar.gz
RUN cd openssl-${OPENSSL_VERSION} && ./Configure --openssldir=/etc/ssl && make -j$(nproc)
FROM dependabot-alpine AS builder
RUN apk add --no-cache 'crystal=1.16.3-r0' shards \
sqlite-static yaml-static yaml-dev \
pcre2-static gc-static \
libxml2-static zlib-static \
openssl-libs-static openssl-dev musl-dev xz-static
ARG release
WORKDIR /invidious
COPY ./shard.yml ./shard.yml
COPY ./shard.lock ./shard.lock
RUN shards install --production
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/
# Required for fetching player dependencies
COPY ./scripts/ ./scripts/
COPY ./assets/ ./assets/
COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
RUN crystal spec --warnings all \
--link-flags "-lxml2 -llzma"
ARG OPENSSL_VERSION
COPY --from=openssl-builder /openssl-${OPENSSL_VERSION} /openssl-${OPENSSL_VERSION}
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
PKG_CONFIG_PATH=/openssl-${OPENSSL_VERSION} \
crystal build ./src/invidious.cr \
--release \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
else \
PKG_CONFIG_PATH=/openssl-${OPENSSL_VERSION} \
crystal build ./src/invidious.cr \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
fi
FROM alpine:3.22
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious
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: invidious-db/' config/config.yml
COPY ./config/sql/ ./config/sql/
COPY ./locales/ ./locales/
COPY --from=builder /invidious/assets ./assets/
COPY --from=builder /invidious/invidious .
RUN chmod o+rX -R ./assets ./config ./locales
EXPOSE 3000
USER invidious
ENTRYPOINT ["/sbin/tini", "--"]
CMD [ "/invidious/invidious" ]

View File

@ -24,7 +24,7 @@ def create_licence_tr(path, file_name, licence_name, licence_link, source_locati
"<tr> "<tr>
<td><a href=\\"/#{path}\\">#{file_name}</a></td> <td><a href=\\"/#{path}\\">#{file_name}</a></td>
<td><a href=\\"#{licence_link}\\">#{licence_name}</a></td> <td><a href=\\"#{licence_link}\\">#{licence_name}</a></td>
<td><a href=\\"#{source_location}\\">\#{translate(locale, "source")}</a></td> <td><a href=\\"#{source_location}\\">\#{I18n.translate(locale, "source")}</a></td>
</tr>" </tr>"
HTML HTML

View File

@ -38,7 +38,7 @@ struct ChannelVideo
json.field "authorId", self.ucid json.field "authorId", self.ucid
json.field "authorUrl", "/channel/#{self.ucid}" json.field "authorUrl", "/channel/#{self.ucid}"
json.field "published", self.published.to_unix json.field "published", self.published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale)) json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "viewCount", self.views json.field "viewCount", self.views
end end

View File

@ -127,11 +127,11 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing
reply_count = short_text_to_number(post.dig?("actionButtons", "commentActionButtonsRenderer", "replyButton", "buttonRenderer", "text", "simpleText").try &.as_s || "0") reply_count = short_text_to_number(post.dig?("actionButtons", "commentActionButtonsRenderer", "replyButton", "buttonRenderer", "text", "simpleText").try &.as_s || "0")
json.field "content", html_to_content(content_html) json.field "content", Helpers.html_to_content(content_html)
json.field "contentHtml", content_html json.field "contentHtml", content_html
json.field "published", published.to_unix json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(published, locale))
json.field "likeCount", like_count json.field "likeCount", like_count
json.field "replyCount", reply_count json.field "replyCount", reply_count

View File

@ -254,7 +254,7 @@ module Invidious::Comments
end end
content_html = html_content || "" content_html = html_content || ""
json.field "content", html_to_content(content_html) json.field "content", Helpers.html_to_content(content_html)
json.field "contentHtml", content_html json.field "contentHtml", content_html
if published_text != nil if published_text != nil
@ -268,7 +268,7 @@ module Invidious::Comments
end end
json.field "published", published.to_unix json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(published, locale))
end end
if node_replies && !response["commentRepliesContinuation"]? if node_replies && !response["commentRepliesContinuation"]?

View File

@ -28,14 +28,14 @@ module Invidious::Frontend::ChannelPage
if tab == selected_tab if tab == selected_tab
str << "\t<b>" str << "\t<b>"
str << translate(locale, "channel_tab_#{tab_name}_label") str << I18n.translate(locale, "channel_tab_#{tab_name}_label")
str << "</b>\n" str << "</b>\n"
else else
# Video tab doesn't have the last path component # Video tab doesn't have the last path component
url = tab.videos? ? base_url : "#{base_url}/#{tab_name}" url = tab.videos? ? base_url : "#{base_url}/#{tab_name}"
str << %(\t<a href=") << url << %(">) str << %(\t<a href=") << url << %(">)
str << translate(locale, "channel_tab_#{tab_name}_label") str << I18n.translate(locale, "channel_tab_#{tab_name}_label")
str << "</a>\n" str << "</a>\n"
end end

View File

@ -32,9 +32,9 @@ module Invidious::Frontend::Comments
<p> <p>
<a href="javascript:void(0)" data-onclick="toggle_parent">[ ]</a> <a href="javascript:void(0)" data-onclick="toggle_parent">[ ]</a>
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b> <b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
#{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} #{I18n.translate_count(locale, "comments_points_count", child.score, I18n::NumberFormatting::Separator)}
<span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span> <span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{I18n.translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
<a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a> <a href="https://www.reddit.com#{child.permalink}" title="#{I18n.translate(locale, "permalink")}">#{I18n.translate(locale, "permalink")}</a>
</p> </p>
<div> <div>
#{body_html} #{body_html}

View File

@ -6,10 +6,10 @@ module Invidious::Frontend::Comments
root = comments["comments"].as_a root = comments["comments"].as_a
root.each do |child| root.each do |child|
if child["replies"]? if child["replies"]?
replies_count_text = translate_count(locale, replies_count_text = I18n.translate_count(locale,
"comments_view_x_replies", "comments_view_x_replies",
child["replies"]["replyCount"].as_i64 || 0, child["replies"]["replyCount"].as_i64 || 0,
NumberFormatting::Separator I18n::NumberFormatting::Separator
) )
replies_html = <<-END_HTML replies_html = <<-END_HTML
@ -25,10 +25,10 @@ module Invidious::Frontend::Comments
END_HTML END_HTML
elsif comments["authorId"]? && !comments["singlePost"]? elsif comments["authorId"]? && !comments["singlePost"]?
# for posts we should display a link to the post # for posts we should display a link to the post
replies_count_text = translate_count(locale, replies_count_text = I18n.translate_count(locale,
"comments_view_x_replies", "comments_view_x_replies",
child["replyCount"].as_i64 || 0, child["replyCount"].as_i64 || 0,
NumberFormatting::Separator I18n::NumberFormatting::Separator
) )
replies_html = <<-END_HTML replies_html = <<-END_HTML
@ -61,7 +61,7 @@ module Invidious::Frontend::Comments
sponsor_icon = String.build do |str| sponsor_icon = String.build do |str|
str << %(<img alt="" ) str << %(<img alt="" )
str << %(src="/ggpht) << URI.parse(child["sponsorIconUrl"].as_s).request_target << "\" " str << %(src="/ggpht) << URI.parse(child["sponsorIconUrl"].as_s).request_target << "\" "
str << %(title=") << translate(locale, "Channel Sponsor") << "\" " str << %(title=") << I18n.translate(locale, "Channel Sponsor") << "\" "
str << %(width="16" height="16" />) str << %(width="16" height="16" />)
end end
end end
@ -110,14 +110,14 @@ module Invidious::Frontend::Comments
when "multiImage" when "multiImage"
html << <<-END_HTML html << <<-END_HTML
<section class="carousel"> <section class="carousel">
<a class="skip-link" href="#skip-#{child["commentId"]}">#{translate(locale, "carousel_skip")}</a> <a class="skip-link" href="#skip-#{child["commentId"]}">#{I18n.translate(locale, "carousel_skip")}</a>
<div class="slides"> <div class="slides">
END_HTML END_HTML
image_array = attachment["images"].as_a image_array = attachment["images"].as_a
image_array.each_index do |i| image_array.each_index do |i|
html << <<-END_HTML html << <<-END_HTML
<div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0"> <div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{I18n.translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0">
<img loading="lazy" src="/ggpht#{URI.parse(image_array[i][1]["url"].as_s).request_target}" alt="" /> <img loading="lazy" src="/ggpht#{URI.parse(image_array[i][1]["url"].as_s).request_target}" alt="" />
</div> </div>
END_HTML END_HTML
@ -129,7 +129,7 @@ module Invidious::Frontend::Comments
END_HTML END_HTML
attachment["images"].as_a.each_index do |i| attachment["images"].as_a.each_index do |i|
html << <<-END_HTML html << <<-END_HTML
<a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a> <a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{I18n.translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a>
END_HTML END_HTML
end end
html << <<-END_HTML html << <<-END_HTML
@ -143,18 +143,18 @@ module Invidious::Frontend::Comments
html << <<-END_HTML html << <<-END_HTML
<p> <p>
<span title="#{Time.unix(child["published"].as_i64).to_s(translate(locale, "%A %B %-d, %Y"))}">#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span> <span title="#{Time.unix(child["published"].as_i64).to_s(I18n.translate(locale, "%A %B %-d, %Y"))}">#{I18n.translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? I18n.translate(locale, "(edited)") : ""}</span>
| |
END_HTML END_HTML
if comments["videoId"]? if comments["videoId"]?
html << <<-END_HTML html << <<-END_HTML
<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a> <a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{I18n.translate(locale, "YouTube comment permalink")}">[YT]</a>
| |
END_HTML END_HTML
elsif comments["authorId"]? elsif comments["authorId"]?
html << <<-END_HTML html << <<-END_HTML
<a rel="noreferrer noopener" href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a> <a rel="noreferrer noopener" href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{I18n.translate(locale, "YouTube comment permalink")}">[YT]</a>
| |
END_HTML END_HTML
end end
@ -172,7 +172,7 @@ module Invidious::Frontend::Comments
html << <<-END_HTML html << <<-END_HTML
&nbsp; &nbsp;
<span class="creator-heart-container" title="#{translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}"> <span class="creator-heart-container" title="#{I18n.translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}">
<span class="creator-heart"> <span class="creator-heart">
<img loading="lazy" class="creator-heart-background-hearted" src="#{creator_thumbnail}" alt="" /> <img loading="lazy" class="creator-heart-background-hearted" src="#{creator_thumbnail}" alt="" />
<span class="creator-heart-small-hearted"> <span class="creator-heart-small-hearted">
@ -197,7 +197,7 @@ module Invidious::Frontend::Comments
<div class="pure-u-1"> <div class="pure-u-1">
<p> <p>
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}" <a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{translate(locale, "Load more")}</a> data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{I18n.translate(locale, "Load more")}</a>
</p> </p>
</div> </div>
</div> </div>

View File

@ -6,16 +6,16 @@ module Invidious::Frontend::Pagination
private def first_page(str : String::Builder, locale : String?, url : String) private def first_page(str : String::Builder, locale : String?, url : String)
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
if locale_is_rtl?(locale) if I18n.locale_is_rtl?(locale)
# Inverted arrow ("first" points to the right) # Inverted arrow ("first" points to the right)
str << translate(locale, "First page") str << I18n.translate(locale, "First page")
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << %(<i class="icon ion-ios-arrow-forward"></i>) str << %(<i class="icon ion-ios-arrow-forward"></i>)
else else
# Regular arrow ("first" points to the left) # Regular arrow ("first" points to the left)
str << %(<i class="icon ion-ios-arrow-back"></i>) str << %(<i class="icon ion-ios-arrow-back"></i>)
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << translate(locale, "First page") str << I18n.translate(locale, "First page")
end end
str << "</a>" str << "</a>"
@ -25,16 +25,16 @@ module Invidious::Frontend::Pagination
# Link # Link
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
if locale_is_rtl?(locale) if I18n.locale_is_rtl?(locale)
# Inverted arrow ("previous" points to the right) # Inverted arrow ("previous" points to the right)
str << translate(locale, "Previous page") str << I18n.translate(locale, "Previous page")
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << %(<i class="icon ion-ios-arrow-forward"></i>) str << %(<i class="icon ion-ios-arrow-forward"></i>)
else else
# Regular arrow ("previous" points to the left) # Regular arrow ("previous" points to the left)
str << %(<i class="icon ion-ios-arrow-back"></i>) str << %(<i class="icon ion-ios-arrow-back"></i>)
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << translate(locale, "Previous page") str << I18n.translate(locale, "Previous page")
end end
str << "</a>" str << "</a>"
@ -44,14 +44,14 @@ module Invidious::Frontend::Pagination
# Link # Link
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">) str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
if locale_is_rtl?(locale) if I18n.locale_is_rtl?(locale)
# Inverted arrow ("next" points to the left) # Inverted arrow ("next" points to the left)
str << %(<i class="icon ion-ios-arrow-back"></i>) str << %(<i class="icon ion-ios-arrow-back"></i>)
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << translate(locale, "Next page") str << I18n.translate(locale, "Next page")
else else
# Regular arrow ("next" points to the right) # Regular arrow ("next" points to the right)
str << translate(locale, "Next page") str << I18n.translate(locale, "Next page")
str << "&nbsp;&nbsp;" str << "&nbsp;&nbsp;"
str << %(<i class="icon ion-ios-arrow-forward"></i>) str << %(<i class="icon ion-ios-arrow-forward"></i>)
end end

View File

@ -6,7 +6,7 @@ module Invidious::Frontend::SearchFilters
return String.build(8000) do |str| return String.build(8000) do |str|
str << "<div id='filters'>\n" str << "<div id='filters'>\n"
str << "\t<details id='filters-collapse'>" str << "\t<details id='filters-collapse'>"
str << "\t\t<summary>" << translate(locale, "search_filters_title") << "</summary>\n" str << "\t\t<summary>" << I18n.translate(locale, "search_filters_title") << "</summary>\n"
str << "\t\t<div id='filters-box'><form action='/search' method='get'>\n" str << "\t\t<div id='filters-box'><form action='/search' method='get'>\n"
@ -25,7 +25,7 @@ module Invidious::Frontend::SearchFilters
str << "\t\t\t<div id='filters-apply'>" str << "\t\t\t<div id='filters-apply'>"
str << "<button type='submit' class=\"pure-button pure-button-primary\">" str << "<button type='submit' class=\"pure-button pure-button-primary\">"
str << translate(locale, "search_filters_apply_button") str << I18n.translate(locale, "search_filters_apply_button")
str << "</button></div>\n" str << "</button></div>\n"
str << "\t\t</form></div>\n" str << "\t\t</form></div>\n"
@ -41,7 +41,7 @@ module Invidious::Frontend::SearchFilters
str << "\t\t\t\t<div class=\"filter-column\"><fieldset>\n" str << "\t\t\t\t<div class=\"filter-column\"><fieldset>\n"
str << "\t\t\t\t\t<legend><div class=\"filter-name underlined\">" str << "\t\t\t\t\t<legend><div class=\"filter-name underlined\">"
str << translate(locale, "search_filters_{{name}}_label") str << I18n.translate(locale, "search_filters_{{name}}_label")
str << "</div></legend>\n" str << "</div></legend>\n"
str << "\t\t\t\t\t<div class=\"filter-options\">\n" str << "\t\t\t\t\t<div class=\"filter-options\">\n"
@ -62,7 +62,7 @@ module Invidious::Frontend::SearchFilters
str << '>' str << '>'
str << "<label for='filter-date-{{date}}'>" str << "<label for='filter-date-{{date}}'>"
str << translate(locale, "search_filters_date_option_{{date}}") str << I18n.translate(locale, "search_filters_date_option_{{date}}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end
@ -78,7 +78,7 @@ module Invidious::Frontend::SearchFilters
str << '>' str << '>'
str << "<label for='filter-type-{{type}}'>" str << "<label for='filter-type-{{type}}'>"
str << translate(locale, "search_filters_type_option_{{type}}") str << I18n.translate(locale, "search_filters_type_option_{{type}}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end
@ -94,7 +94,7 @@ module Invidious::Frontend::SearchFilters
str << '>' str << '>'
str << "<label for='filter-duration-{{duration}}'>" str << "<label for='filter-duration-{{duration}}'>"
str << translate(locale, "search_filters_duration_option_{{duration}}") str << I18n.translate(locale, "search_filters_duration_option_{{duration}}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end
@ -111,7 +111,7 @@ module Invidious::Frontend::SearchFilters
str << '>' str << '>'
str << "<label for='filter-feature-{{feature}}'>" str << "<label for='filter-feature-{{feature}}'>"
str << translate(locale, "search_filters_features_option_{{feature}}") str << I18n.translate(locale, "search_filters_features_option_{{feature}}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
{% end %} {% end %}
@ -128,7 +128,7 @@ module Invidious::Frontend::SearchFilters
str << '>' str << '>'
str << "<label for='filter-sort-{{sort}}'>" str << "<label for='filter-sort-{{sort}}'>"
str << translate(locale, "search_filters_sort_option_{{sort}}") str << I18n.translate(locale, "search_filters_sort_option_{{sort}}")
str << "</label></div>\n" str << "</label></div>\n"
{% end %} {% end %}
end end

View File

@ -20,11 +20,11 @@ module Invidious::Frontend::WatchPage
def download_widget(locale : String, video : Video, video_assets : VideoAssets) : String def download_widget(locale : String, video : Video, video_assets : VideoAssets) : String
if CONFIG.disabled?("downloads") if CONFIG.disabled?("downloads")
return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>" return "<p id=\"download\">#{I18n.translate(locale, "Download is disabled")}</p>"
end end
if CONFIG.dmca_content.includes?(video.id) if CONFIG.dmca_content.includes?(video.id)
return "<p id=\"download\">#{translate(locale, "dmca_content")}</p>" return "<p id=\"download\">#{I18n.translate(locale, "dmca_content")}</p>"
end end
url = "/download" url = "/download"
@ -49,7 +49,7 @@ module Invidious::Frontend::WatchPage
str << "\t<div class=\"pure-control-group\">\n" str << "\t<div class=\"pure-control-group\">\n"
str << "\t\t<label for='download_widget'>" str << "\t\t<label for='download_widget'>"
str << translate(locale, "Download as: ") str << I18n.translate(locale, "Download as: ")
str << "</label>\n" str << "</label>\n"
str << "\t\t<select name='download_widget' id='download_widget'>\n" str << "\t\t<select name='download_widget' id='download_widget'>\n"
@ -98,7 +98,7 @@ module Invidious::Frontend::WatchPage
value = {"label": caption.name, "ext": "#{caption.language_code}.vtt"}.to_json value = {"label": caption.name, "ext": "#{caption.language_code}.vtt"}.to_json
str << "\t\t\t<option value='" << value << "'>" str << "\t\t\t<option value='" << value << "'>"
str << translate(locale, "download_subtitles", translate(locale, caption.name)) str << I18n.translate(locale, "download_subtitles", I18n.translate(locale, caption.name))
str << "</option>\n" str << "</option>\n"
end end
@ -108,7 +108,7 @@ module Invidious::Frontend::WatchPage
str << "\t</div>\n" str << "\t</div>\n"
str << "\t<button type=\"submit\" class=\"pure-button pure-button-primary\">\n" str << "\t<button type=\"submit\" class=\"pure-button pure-button-primary\">\n"
str << "\t\t<b>" << translate(locale, "Download") << "</b>\n" str << "\t\t<b>" << I18n.translate(locale, "Download") << "</b>\n"
str << "\t</button>\n" str << "\t</button>\n"
str << "</form>\n" str << "</form>\n"

View File

@ -63,19 +63,19 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
error_message = <<-END_HTML error_message = <<-END_HTML
<div class="error_message"> <div class="error_message">
<h2>#{translate(locale, "crash_page_you_found_a_bug")}</h2> <h2>#{I18n.translate(locale, "crash_page_you_found_a_bug")}</h2>
<br/><br/> <br/><br/>
<p><b>#{translate(locale, "crash_page_before_reporting")}</b></p> <p><b>#{I18n.translate(locale, "crash_page_before_reporting")}</b></p>
<ul> <ul>
<li>#{translate(locale, "crash_page_refresh", env.request.resource)}</li> <li>#{I18n.translate(locale, "crash_page_refresh", env.request.resource)}</li>
<li>#{translate(locale, "crash_page_switch_instance", url_switch)}</li> <li>#{I18n.translate(locale, "crash_page_switch_instance", url_switch)}</li>
<li>#{translate(locale, "crash_page_read_the_faq", url_faq)}</li> <li>#{I18n.translate(locale, "crash_page_read_the_faq", url_faq)}</li>
<li>#{translate(locale, "crash_page_search_issue", url_search_issues)}</li> <li>#{I18n.translate(locale, "crash_page_search_issue", url_search_issues)}</li>
</ul> </ul>
<br/> <br/>
<p>#{translate(locale, "crash_page_report_issue", url_new_issue)}</p> <p>#{I18n.translate(locale, "crash_page_report_issue", url_new_issue)}</p>
<!-- TODO: Add a "copy to clipboard" button --> <!-- TODO: Add a "copy to clipboard" button -->
<pre class="error-issue-template">#{issue_template}</pre> <pre class="error-issue-template">#{issue_template}</pre>
@ -95,7 +95,7 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, mess
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
error_message = translate(locale, message) error_message = I18n.translate(locale, message)
next_steps = error_redirect_helper(env) next_steps = error_redirect_helper(env)
return templated "error" return templated "error"
@ -186,10 +186,10 @@ def error_redirect_helper(env : HTTP::Server::Context)
if request_path.starts_with?("/search") || request_path.starts_with?("/watch") || if request_path.starts_with?("/search") || request_path.starts_with?("/watch") ||
request_path.starts_with?("/channel") || request_path.starts_with?("/playlist?list=PL") request_path.starts_with?("/channel") || request_path.starts_with?("/playlist?list=PL")
next_steps_text = translate(locale, "next_steps_error_message") next_steps_text = I18n.translate(locale, "next_steps_error_message")
refresh = translate(locale, "next_steps_error_message_refresh") refresh = I18n.translate(locale, "next_steps_error_message_refresh")
go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube") go_to_youtube = I18n.translate(locale, "next_steps_error_message_go_to_youtube")
switch_instance = translate(locale, "Switch Invidious Instance") switch_instance = I18n.translate(locale, "Switch Invidious Instance")
return <<-END_HTML return <<-END_HTML
<p style="margin-bottom: 4px;">#{next_steps_text}</p> <p style="margin-bottom: 4px;">#{next_steps_text}</p>

View File

@ -1,7 +1,5 @@
require "./macros" require "./macros"
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
struct Nonce struct Nonce
include DB::Serializable include DB::Serializable
@ -24,60 +22,124 @@ struct Annotation
property annotations : String property annotations : String
end end
def html_to_content(description_html : String) module Helpers
description = description_html.gsub(/(<br>)|(<br\/>)/, { extend self
"<br>": "\n",
"<br/>": "\n",
})
if !description.empty? private TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
description = XML.parse_html(description).content.strip("\n ")
end
return description def html_to_content(description_html : String)
end description = description_html.gsub(/(<br>)|(<br\/>)/, {
"<br>": "\n",
"<br/>": "\n",
})
def cache_annotation(id, annotations) if !description.empty?
if !CONFIG.cache_annotations description = XML.parse_html(description).content.strip("\n ")
return
end
body = XML.parse(annotations)
nodeset = body.xpath_nodes(%q(/document/annotations/annotation))
return if nodeset == 0
has_legacy_annotations = false
nodeset.each do |node|
if !{"branding", "card", "drawer"}.includes? node["type"]?
has_legacy_annotations = true
break
end end
return description
end end
Invidious::Database::Annotations.insert(id, annotations) if has_legacy_annotations def cache_annotation(id, annotations)
end if !CONFIG.cache_annotations
return
end
def create_notification_stream(env, topics, connection_channel) body = XML.parse(annotations)
connection = Channel(PQ::Notification).new(8) nodeset = body.xpath_nodes(%q(/document/annotations/annotation))
connection_channel.send({true, connection})
locale = env.get("preferences").as(Preferences).locale return if nodeset == 0
since = env.params.query["since"]?.try &.to_i? has_legacy_annotations = false
id = 0 nodeset.each do |node|
if !{"branding", "card", "drawer"}.includes? node["type"]?
has_legacy_annotations = true
break
end
end
Invidious::Database::Annotations.insert(id, annotations) if has_legacy_annotations
end
def create_notification_stream(env, topics, connection_channel)
connection = Channel(PQ::Notification).new(8)
connection_channel.send({true, connection})
locale = env.get("preferences").as(Preferences).locale
since = env.params.query["since"]?.try &.to_i?
id = 0
if topics.includes? "debug"
spawn do
begin
loop do
time_span = [0, 0, 0, 0]
time_span[rand(4)] = rand(30) + 5
published = Time.utc - Time::Span.new(days: time_span[0], hours: time_span[1], minutes: time_span[2], seconds: time_span[3])
video_id = TEST_IDS[rand(TEST_IDS.size)]
video = get_video(video_id)
video.published = published
response = JSON.parse(video.to_json(locale, nil))
env.response.puts "id: #{id}"
env.response.puts "data: #{response.to_json}"
env.response.puts
env.response.flush
id += 1
sleep 1.minute
Fiber.yield
end
rescue ex
end
end
end
spawn do
begin
if since
since_unix = Time.unix(since.not_nil!)
topics.try &.each do |topic|
case topic
when .match(/UC[A-Za-z0-9_-]{22}/)
Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video|
response = JSON.parse(video.to_json(locale))
env.response.puts "id: #{id}"
env.response.puts "data: #{response.to_json}"
env.response.puts
env.response.flush
id += 1
end
else
# TODO
end
end
end
end
end
if topics.includes? "debug"
spawn do spawn do
begin begin
loop do loop do
time_span = [0, 0, 0, 0] event = connection.receive
time_span[rand(4)] = rand(30) + 5
published = Time.utc - Time::Span.new(days: time_span[0], hours: time_span[1], minutes: time_span[2], seconds: time_span[3]) notification = JSON.parse(event.payload)
video_id = TEST_IDS[rand(TEST_IDS.size)] topic = notification["topic"].as_s
video_id = notification["videoId"].as_s
published = notification["published"].as_i64
if !topics.try &.includes? topic
next
end
video = get_video(video_id) video = get_video(video_id)
video.published = published video.published = Time.unix(published)
response = JSON.parse(video.to_json(locale, nil)) response = JSON.parse(video.to_json(locale, nil))
env.response.puts "id: #{id}" env.response.puts "id: #{id}"
@ -86,65 +148,20 @@ def create_notification_stream(env, topics, connection_channel)
env.response.flush env.response.flush
id += 1 id += 1
sleep 1.minute
Fiber.yield
end end
rescue ex rescue ex
ensure
connection_channel.send({false, connection})
end end
end end
end
spawn do
begin
if since
since_unix = Time.unix(since.not_nil!)
topics.try &.each do |topic|
case topic
when .match(/UC[A-Za-z0-9_-]{22}/)
Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video|
response = JSON.parse(video.to_json(locale))
env.response.puts "id: #{id}"
env.response.puts "data: #{response.to_json}"
env.response.puts
env.response.flush
id += 1
end
else
# TODO
end
end
end
end
end
spawn do
begin begin
# Send heartbeat
loop do loop do
event = connection.receive env.response.puts ":keepalive #{Time.utc.to_unix}"
notification = JSON.parse(event.payload)
topic = notification["topic"].as_s
video_id = notification["videoId"].as_s
published = notification["published"].as_i64
if !topics.try &.includes? topic
next
end
video = get_video(video_id)
video.published = Time.unix(published)
response = JSON.parse(video.to_json(locale, nil))
env.response.puts "id: #{id}"
env.response.puts "data: #{response.to_json}"
env.response.puts env.response.puts
env.response.flush env.response.flush
sleep (20 + rand(11)).seconds
id += 1
end end
rescue ex rescue ex
ensure ensure
@ -152,51 +169,38 @@ def create_notification_stream(env, topics, connection_channel)
end end
end end
begin def extract_initial_data(body) : Hash(String, JSON::Any)
# Send heartbeat return JSON.parse(body.match(/(window\["ytInitialData"\]|var\s*ytInitialData)\s*=\s*(?<info>{.*?});<\/script>/mx).try &.["info"] || "{}").as_h
loop do
env.response.puts ":keepalive #{Time.utc.to_unix}"
env.response.puts
env.response.flush
sleep (20 + rand(11)).seconds
end
rescue ex
ensure
connection_channel.send({false, connection})
end
end
def extract_initial_data(body) : Hash(String, JSON::Any)
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)
if response.headers.includes_word?("Content-Encoding", "gzip")
Compress::Gzip::Writer.open(env.response) do |deflate|
IO.copy response.body_io, deflate
end
elsif response.headers.includes_word?("Content-Encoding", "deflate")
Compress::Deflate::Writer.open(env.response) do |deflate|
IO.copy response.body_io, deflate
end
else
IO.copy response.body_io, env.response
end
end
# Fetch the playback requests tracker from the statistics endpoint.
#
# Creates a new tracker when unavailable.
def get_playback_statistic
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]) && tracker.as(Hash).empty?
tracker = {
"totalRequests" => 0_i64,
"successfulRequests" => 0_i64,
"ratio" => 0_f64,
}
Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"] = tracker
end end
return tracker.as(Hash(String, Int64 | Float64)) def proxy_file(response, env)
if response.headers.includes_word?("Content-Encoding", "gzip")
Compress::Gzip::Writer.open(env.response) do |deflate|
IO.copy response.body_io, deflate
end
elsif response.headers.includes_word?("Content-Encoding", "deflate")
Compress::Deflate::Writer.open(env.response) do |deflate|
IO.copy response.body_io, deflate
end
else
IO.copy response.body_io, env.response
end
end
# Fetch the playback requests tracker from the statistics endpoint.
#
# Creates a new tracker when unavailable.
def get_playback_statistic
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]) && tracker.as(Hash).empty?
tracker = {
"totalRequests" => 0_i64,
"successfulRequests" => 0_i64,
"ratio" => 0_f64,
}
Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"] = tracker
end
return tracker.as(Hash(String, Int64 | Float64))
end
end end

View File

@ -1,199 +1,203 @@
# Languages requiring a better level of translation (at least 20%) module I18n
# to be added to the list below: extend self
#
# "af" => "", # Afrikaans
# "az" => "", # Azerbaijani
# "be" => "", # Belarusian
# "bn_BD" => "", # Bengali (Bangladesh)
# "ia" => "", # Interlingua
# "or" => "", # Odia
# "tk" => "", # Turkmen
# "tok => "", # Toki Pona
#
LOCALES_LIST = {
"ar" => "العربية", # Arabic
"bg" => "български", # Bulgarian
"bn" => "বাংলা", # Bengali
"ca" => "Català", # Catalan
"cs" => "Čeština", # Czech
"cy" => "Cymraeg", # Welsh
"da" => "Dansk", # Danish
"de" => "Deutsch", # German
"el" => "Ελληνικά", # Greek
"en-US" => "English", # English
"eo" => "Esperanto", # Esperanto
"es" => "Español", # Spanish
"et" => "Eesti keel", # Estonian
"eu" => "Euskara", # Basque
"fa" => "فارسی", # Persian
"fi" => "Suomi", # Finnish
"fr" => "Français", # French
"he" => "עברית", # Hebrew
"hi" => "हिन्दी", # Hindi
"hr" => "Hrvatski", # Croatian
"hu-HU" => "Magyar Nyelv", # Hungarian
"id" => "Bahasa Indonesia", # Indonesian
"is" => "Íslenska", # Icelandic
"it" => "Italiano", # Italian
"ja" => "日本語", # Japanese
"ko" => "한국어", # Korean
"lmo" => "Lombard", # Lombard
"lt" => "Lietuvių", # Lithuanian
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
"nl" => "Nederlands", # Dutch
"pl" => "Polski", # Polish
"pt" => "Português", # Portuguese
"pt-BR" => "Português Brasileiro", # Portuguese (Brazil)
"pt-PT" => "Português de Portugal", # Portuguese (Portugal)
"ro" => "Română", # Romanian
"ru" => "Русский", # Russian
"si" => "සිංහල", # Sinhala
"sk" => "Slovenčina", # Slovak
"sl" => "Slovenščina", # Slovenian
"sq" => "Shqip", # Albanian
"sr" => "Srpski (latinica)", # Serbian (Latin)
"sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
"sv-SE" => "Svenska", # Swedish
"ta" => "தமிழ்", # Tamil
"tr" => "Türkçe", # Turkish
"uk" => "Українська", # Ukrainian
"vi" => "Tiếng Việt", # Vietnamese
"zh-CN" => "汉语", # Chinese (Simplified)
"zh-TW" => "漢語", # Chinese (Traditional)
}
LOCALES = load_all_locales() # Languages requiring a better level of translation (at least 20%)
# to be added to the list below:
#
# "af" => "", # Afrikaans
# "az" => "", # Azerbaijani
# "be" => "", # Belarusian
# "bn_BD" => "", # Bengali (Bangladesh)
# "ia" => "", # Interlingua
# "or" => "", # Odia
# "tk" => "", # Turkmen
# "tok => "", # Toki Pona
#
LOCALES_LIST = {
"ar" => "العربية", # Arabic
"bg" => "български", # Bulgarian
"bn" => "বাংলা", # Bengali
"ca" => "Català", # Catalan
"cs" => "Čeština", # Czech
"cy" => "Cymraeg", # Welsh
"da" => "Dansk", # Danish
"de" => "Deutsch", # German
"el" => "Ελληνικά", # Greek
"en-US" => "English", # English
"eo" => "Esperanto", # Esperanto
"es" => "Español", # Spanish
"et" => "Eesti keel", # Estonian
"eu" => "Euskara", # Basque
"fa" => "فارسی", # Persian
"fi" => "Suomi", # Finnish
"fr" => "Français", # French
"he" => "עברית", # Hebrew
"hi" => "हिन्दी", # Hindi
"hr" => "Hrvatski", # Croatian
"hu-HU" => "Magyar Nyelv", # Hungarian
"id" => "Bahasa Indonesia", # Indonesian
"is" => "Íslenska", # Icelandic
"it" => "Italiano", # Italian
"ja" => "日本語", # Japanese
"ko" => "한국어", # Korean
"lmo" => "Lombard", # Lombard
"lt" => "Lietuvių", # Lithuanian
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
"nl" => "Nederlands", # Dutch
"pl" => "Polski", # Polish
"pt" => "Português", # Portuguese
"pt-BR" => "Português Brasileiro", # Portuguese (Brazil)
"pt-PT" => "Português de Portugal", # Portuguese (Portugal)
"ro" => "Română", # Romanian
"ru" => "Русский", # Russian
"si" => "සිංහල", # Sinhala
"sk" => "Slovenčina", # Slovak
"sl" => "Slovenščina", # Slovenian
"sq" => "Shqip", # Albanian
"sr" => "Srpski (latinica)", # Serbian (Latin)
"sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
"sv-SE" => "Svenska", # Swedish
"ta" => "தமிழ்", # Tamil
"tr" => "Türkçe", # Turkish
"uk" => "Українська", # Ukrainian
"vi" => "Tiếng Việt", # Vietnamese
"zh-CN" => "汉语", # Chinese (Simplified)
"zh-TW" => "漢語", # Chinese (Traditional)
}
CONTENT_REGIONS = { LOCALES = load_all_locales()
"AE", "AR", "AT", "AU", "AZ", "BA", "BD", "BE", "BG", "BH", "BO", "BR", "BY",
"CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE",
"EG", "ES", "FI", "FR", "GB", "GE", "GH", "GR", "GT", "HK", "HN", "HR", "HU",
"ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "JP", "KE", "KR", "KW",
"KZ", "LB", "LI", "LK", "LT", "LU", "LV", "LY", "MA", "ME", "MK", "MT", "MX",
"MY", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK",
"PL", "PR", "PT", "PY", "QA", "RO", "RS", "RU", "SA", "SE", "SG", "SI", "SK",
"SN", "SV", "TH", "TN", "TR", "TW", "TZ", "UA", "UG", "US", "UY", "VE", "VN",
"YE", "ZA", "ZW",
}
# Enum for the different types of number formats CONTENT_REGIONS = {
enum NumberFormatting "AE", "AR", "AT", "AU", "AZ", "BA", "BD", "BE", "BG", "BH", "BO", "BR", "BY",
None # Print the number as-is "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE",
Separator # Use a separator for thousands "EG", "ES", "FI", "FR", "GB", "GE", "GH", "GR", "GT", "HK", "HN", "HR", "HU",
Short # Use short notation (k/M/B) "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "JP", "KE", "KR", "KW",
HtmlSpan # Surround with <span id="count"></span> "KZ", "LB", "LI", "LK", "LT", "LU", "LV", "LY", "MA", "ME", "MK", "MT", "MX",
end "MY", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK",
"PL", "PR", "PT", "PY", "QA", "RO", "RS", "RU", "SA", "SE", "SG", "SI", "SK",
"SN", "SV", "TH", "TN", "TR", "TW", "TZ", "UA", "UG", "US", "UY", "VE", "VN",
"YE", "ZA", "ZW",
}
def load_all_locales # Enum for the different types of number formats
locales = {} of String => Hash(String, JSON::Any) enum NumberFormatting
None # Print the number as-is
LOCALES_LIST.each_key do |name| Separator # Use a separator for thousands
locales[name] = JSON.parse(File.read("locales/#{name}.json")).as_h Short # Use short notation (k/M/B)
HtmlSpan # Surround with <span id="count"></span>
end end
return locales def load_all_locales
end locales = {} of String => Hash(String, JSON::Any)
def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String LOCALES_LIST.each_key do |name|
# Log a warning if "key" doesn't exist in en-US locale and return locales[name] = JSON.parse(File.read("locales/#{name}.json")).as_h
# that key as the text, so this is more or less transparent to the user.
if !LOCALES["en-US"].has_key?(key)
LOGGER.warn("i18n: Missing translation key \"#{key}\"")
return key
end
# Default to english, whenever the locale doesn't exist,
# or the key requested has not been translated
if locale && LOCALES.has_key?(locale) && LOCALES[locale].has_key?(key)
raw_data = LOCALES[locale][key]
else
raw_data = LOCALES["en-US"][key]
end
case raw_data
when .as_h?
# Init
translation = ""
match_length = 0
raw_data.as_h.each do |hash_key, value|
if text.is_a?(String)
if md = text.try &.match(/#{hash_key}/)
if md[0].size >= match_length
translation = value.as_s
match_length = md[0].size
end
end
end
end end
when .as_s?
translation = raw_data.as_s return locales
else
raise "Invalid translation \"#{raw_data}\""
end end
if text.is_a?(String) def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String
translation = translation.gsub("`x`", text) # Log a warning if "key" doesn't exist in en-US locale and return
elsif text.is_a?(Hash(String, String)) # that key as the text, so this is more or less transparent to the user.
# adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic if !LOCALES["en-US"].has_key?(key)
text.each_key do |hash_key|
translation = translation.gsub("{{#{hash_key}}}", text[hash_key])
end
end
return translation
end
def translate_count(locale : String, key : String, count : Int, format = NumberFormatting::None) : String
# Fallback on english if locale doesn't exist
locale = "en-US" if !LOCALES.has_key?(locale)
# Retrieve suffix
suffix = I18next::Plurals::RESOLVER.get_suffix(locale, count)
plural_key = key + suffix
if LOCALES[locale].has_key?(plural_key)
translation = LOCALES[locale][plural_key].as_s
else
# Try #1: Fallback to singular in the same locale
singular_suffix = I18next::Plurals::RESOLVER.get_suffix(locale, 1)
if LOCALES[locale].has_key?(key + singular_suffix)
translation = LOCALES[locale][key + singular_suffix].as_s
elsif locale != "en-US"
# Try #2: Fallback to english
translation = translate_count("en-US", key, count)
else
# Return key if we're already in english, as the translation is missing
LOGGER.warn("i18n: Missing translation key \"#{key}\"") LOGGER.warn("i18n: Missing translation key \"#{key}\"")
return key return key
end end
# Default to english, whenever the locale doesn't exist,
# or the key requested has not been translated
if locale && LOCALES.has_key?(locale) && LOCALES[locale].has_key?(key)
raw_data = LOCALES[locale][key]
else
raw_data = LOCALES["en-US"][key]
end
case raw_data
when .as_h?
# Init
translation = ""
match_length = 0
raw_data.as_h.each do |hash_key, value|
if text.is_a?(String)
if md = text.try &.match(/#{hash_key}/)
if md[0].size >= match_length
translation = value.as_s
match_length = md[0].size
end
end
end
end
when .as_s?
translation = raw_data.as_s
else
raise "Invalid translation \"#{raw_data}\""
end
if text.is_a?(String)
translation = translation.gsub("`x`", text)
elsif text.is_a?(Hash(String, String))
# adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic
text.each_key do |hash_key|
translation = translation.gsub("{{#{hash_key}}}", text[hash_key])
end
end
return translation
end end
case format def translate_count(locale : String, key : String, count : Int, format = NumberFormatting::None) : String
when .separator? then count_txt = number_with_separator(count) # Fallback on english if locale doesn't exist
when .short? then count_txt = number_to_short_text(count) locale = "en-US" if !LOCALES.has_key?(locale)
when .html_span? then count_txt = "<span id=\"count\">" + count.to_s + "</span>"
else count_txt = count.to_s # Retrieve suffix
suffix = I18next::Plurals::RESOLVER.get_suffix(locale, count)
plural_key = key + suffix
if LOCALES[locale].has_key?(plural_key)
translation = LOCALES[locale][plural_key].as_s
else
# Try #1: Fallback to singular in the same locale
singular_suffix = I18next::Plurals::RESOLVER.get_suffix(locale, 1)
if LOCALES[locale].has_key?(key + singular_suffix)
translation = LOCALES[locale][key + singular_suffix].as_s
elsif locale != "en-US"
# Try #2: Fallback to english
translation = self.translate_count("en-US", key, count)
else
# Return key if we're already in english, as the translation is missing
LOGGER.warn("i18n: Missing translation key \"#{key}\"")
return key
end
end
case format
when .separator? then count_txt = number_with_separator(count)
when .short? then count_txt = number_to_short_text(count)
when .html_span? then count_txt = "<span id=\"count\">" + count.to_s + "</span>"
else count_txt = count.to_s
end
return translation.gsub("{{count}}", count_txt)
end end
return translation.gsub("{{count}}", count_txt) def translate_bool(locale : String?, translation : Bool)
end case translation
when true
return self.translate(locale, "Yes")
when false
return self.translate(locale, "No")
end
end
def translate_bool(locale : String?, translation : Bool) def locale_is_rtl?(locale : String?)
case translation # Fallback to en-US
when true return false if locale.nil?
return translate(locale, "Yes")
when false # Arabic, Persian, Hebrew
return translate(locale, "No") # See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts
return {"ar", "fa", "he"}.includes? locale
end end
end end
def locale_is_rtl?(locale : String?)
# Fallback to en-US
return false if locale.nil?
# Arabic, Persian, Hebrew
# See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts
return {"ar", "fa", "he"}.includes? locale
end

View File

@ -53,7 +53,7 @@ struct SearchVideo
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg") xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg")
end end
xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text html_to_content(self.description_html) } xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text Helpers.html_to_content(self.description_html) }
end end
end end
@ -63,7 +63,7 @@ struct SearchVideo
xml.element("media:title") { xml.text self.title } xml.element("media:title") { xml.text self.title }
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg", xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg",
width: "320", height: "180") width: "320", height: "180")
xml.element("media:description") { xml.text html_to_content(self.description_html) } xml.element("media:description") { xml.text Helpers.html_to_content(self.description_html) }
end end
xml.element("media:community") do xml.element("media:community") do
@ -111,13 +111,13 @@ struct SearchVideo
Invidious::JSONify::APIv1.thumbnails(json, self.id) Invidious::JSONify::APIv1.thumbnails(json, self.id)
end end
json.field "description", html_to_content(self.description_html) json.field "description", Helpers.html_to_content(self.description_html)
json.field "descriptionHtml", self.description_html json.field "descriptionHtml", self.description_html
json.field "viewCount", self.views json.field "viewCount", self.views
json.field "viewCountText", translate_count(locale, "generic_views_count", self.views, NumberFormatting::Short) json.field "viewCountText", I18n.translate_count(locale, "generic_views_count", self.views, I18n::NumberFormatting::Short)
json.field "published", self.published.to_unix json.field "published", self.published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale)) json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "lengthSeconds", self.length_seconds json.field "lengthSeconds", self.length_seconds
json.field "liveNow", self.badges.live_now? json.field "liveNow", self.badges.live_now?
json.field "premium", self.badges.premium? json.field "premium", self.badges.premium?
@ -255,7 +255,7 @@ struct SearchChannel
json.field "videoCount", self.video_count json.field "videoCount", self.video_count
json.field "channelHandle", self.channel_handle json.field "channelHandle", self.channel_handle
json.field "description", html_to_content(self.description_html) json.field "description", Helpers.html_to_content(self.description_html)
json.field "descriptionHtml", self.description_html json.field "descriptionHtml", self.description_html
end end
end end
@ -327,8 +327,8 @@ struct ProblematicTimelineItem
xml.element("content", type: "xhtml") do xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("div") do xml.element("div") do
xml.element("h4") { translate(locale, "timeline_parse_error_placeholder_heading") } xml.element("h4") { I18n.translate(locale, "timeline_parse_error_placeholder_heading") }
xml.element("p") { translate(locale, "timeline_parse_error_placeholder_message") } xml.element("p") { I18n.translate(locale, "timeline_parse_error_placeholder_message") }
end end
xml.element("pre") do xml.element("pre") do

View File

@ -146,19 +146,19 @@ def recode_date(time : Time, locale)
span = Time.utc - time span = Time.utc - time
if span.total_days > 365.0 if span.total_days > 365.0
return translate_count(locale, "generic_count_years", span.total_days.to_i // 365) return I18n.translate_count(locale, "generic_count_years", span.total_days.to_i // 365)
elsif span.total_days > 30.0 elsif span.total_days > 30.0
return translate_count(locale, "generic_count_months", span.total_days.to_i // 30) return I18n.translate_count(locale, "generic_count_months", span.total_days.to_i // 30)
elsif span.total_days > 7.0 elsif span.total_days > 7.0
return translate_count(locale, "generic_count_weeks", span.total_days.to_i // 7) return I18n.translate_count(locale, "generic_count_weeks", span.total_days.to_i // 7)
elsif span.total_hours > 24.0 elsif span.total_hours > 24.0
return translate_count(locale, "generic_count_days", span.total_days.to_i) return I18n.translate_count(locale, "generic_count_days", span.total_days.to_i)
elsif span.total_minutes > 60.0 elsif span.total_minutes > 60.0
return translate_count(locale, "generic_count_hours", span.total_hours.to_i) return I18n.translate_count(locale, "generic_count_hours", span.total_hours.to_i)
elsif span.total_seconds > 60.0 elsif span.total_seconds > 60.0
return translate_count(locale, "generic_count_minutes", span.total_minutes.to_i) return I18n.translate_count(locale, "generic_count_minutes", span.total_minutes.to_i)
else else
return translate_count(locale, "generic_count_seconds", span.total_seconds.to_i) return I18n.translate_count(locale, "generic_count_seconds", span.total_seconds.to_i)
end end
end end

View File

@ -22,7 +22,7 @@ module Invidious::JSONify::APIv1
json.field "description", video.description json.field "description", video.description
json.field "descriptionHtml", video.description_html json.field "descriptionHtml", video.description_html
json.field "published", video.published.to_unix json.field "published", video.published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published, locale)) json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(video.published, locale))
json.field "keywords", video.keywords json.field "keywords", video.keywords
json.field "viewCount", video.views json.field "viewCount", video.views
@ -268,7 +268,7 @@ module Invidious::JSONify::APIv1
json.field "viewCountText", rv["short_view_count"]? json.field "viewCountText", rv["short_view_count"]?
json.field "published", rv["published"]? json.field "published", rv["published"]?
if rv["published"]?.try &.presence if rv["published"]?.try &.presence
json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale)) json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
else else
json.field "publishedText", "" json.field "publishedText", ""
end end

View File

@ -27,7 +27,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
video_id = "CvFH_6DNRCY" if rdid.starts_with? "OLAK5uy_" video_id = "CvFH_6DNRCY" if rdid.starts_with? "OLAK5uy_"
response = YT_POOL.client &.get("/watch?v=#{video_id}&list=#{rdid}&gl=US&hl=en", headers) response = YT_POOL.client &.get("/watch?v=#{video_id}&list=#{rdid}&gl=US&hl=en", headers)
initial_data = extract_initial_data(response.body) initial_data = Helpers.extract_initial_data(response.body)
if !initial_data["contents"]["twoColumnWatchNextResults"]["playlist"]? if !initial_data["contents"]["twoColumnWatchNextResults"]["playlist"]?
raise InfoException.new("Could not create mix.") raise InfoException.new("Could not create mix.")

View File

@ -199,7 +199,7 @@ struct InvidiousPlaylist
json.field "authorUrl", nil json.field "authorUrl", nil
json.field "authorThumbnails", [] of String json.field "authorThumbnails", [] of String
json.field "description", html_to_content(self.description_html) json.field "description", Helpers.html_to_content(self.description_html)
json.field "descriptionHtml", self.description_html json.field "descriptionHtml", self.description_html
json.field "videoCount", self.video_count json.field "videoCount", self.video_count
@ -384,7 +384,7 @@ def fetch_playlist(plid : String)
video_count = text.gsub(/\D/, "").to_i? || 0 video_count = text.gsub(/\D/, "").to_i? || 0
elsif text.includes? "view" elsif text.includes? "view"
views = text.gsub(/\D/, "").to_i64? || 0_i64 views = text.gsub(/\D/, "").to_i64? || 0_i64
else elsif !text.includes? "Pay to watch"
updated = decode_date(text.lchop("Last updated on ").lchop("Updated ")) updated = decode_date(text.lchop("Last updated on ").lchop("Updated "))
end end
end end
@ -445,7 +445,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
# 100 videos per request # 100 videos per request
ctoken = produce_playlist_continuation(playlist.id, offset) ctoken = produce_playlist_continuation(playlist.id, offset)
initial_data = YoutubeAPI.browse(ctoken) initial_data = YoutubeAPI.browse(ctoken)
videos += extract_playlist_videos(initial_data) videos += extract_playlist_videos(playlist.id, initial_data)
offset += 100 offset += 100
end end
@ -454,7 +454,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
end end
end end
def extract_playlist_videos(initial_data : Hash(String, JSON::Any)) def extract_playlist_videos(playlist_id : String, initial_data : Hash(String, JSON::Any))
videos = [] of PlaylistVideo | ProblematicTimelineItem videos = [] of PlaylistVideo | ProblematicTimelineItem
if initial_data["contents"]? if initial_data["contents"]?
@ -480,9 +480,9 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
contents.try &.each do |item| contents.try &.each do |item|
if i = item["playlistVideoRenderer"]? if i = item["playlistVideoRenderer"]?
video_id = i["navigationEndpoint"]["watchEndpoint"]["videoId"].as_s video_id = i.dig?("navigationEndpoint", "watchEndpoint", "videoId").try &.as_s || i.dig("videoId").as_s
plid = i["navigationEndpoint"]["watchEndpoint"]["playlistId"].as_s plid = i.dig?("navigationEndpoint", "watchEndpoint", "playlistId").try &.as_s || playlist_id
index = i["navigationEndpoint"]["watchEndpoint"]["index"].as_i64 index = i.dig?("navigationEndpoint", "watchEndpoint", "index").try &.as_i64 || i.dig("index", "simpleText").as_s.to_i64
title = i["title"].try { |t| t["simpleText"]? || t["runs"]?.try &.[0]["text"]? }.try &.as_s || "" title = i["title"].try { |t| t["simpleText"]? || t["runs"]?.try &.[0]["text"]? }.try &.as_s || ""
author = i["shortBylineText"]?.try &.["runs"][0]["text"].as_s || "" author = i["shortBylineText"]?.try &.["runs"][0]["text"].as_s || ""

View File

@ -8,7 +8,7 @@ module Invidious::Routes::API::V1::Authenticated
# topics = env.params.body["topics"]?.try &.split(",").uniq.first(1000) # topics = env.params.body["topics"]?.try &.split(",").uniq.first(1000)
# topics ||= [] of String # topics ||= [] of String
# create_notification_stream(env, topics, connection_channel) # Helpers.create_notification_stream(env, topics, connection_channel)
# end # end
def self.get_preferences(env) def self.get_preferences(env)
@ -490,6 +490,6 @@ module Invidious::Routes::API::V1::Authenticated
topics = raw_topics.try &.split(",").uniq.first(1000) topics = raw_topics.try &.split(",").uniq.first(1000)
topics ||= [] of String topics ||= [] of String
create_notification_stream(env, topics, CONNECTION_CHANNEL) Helpers.create_notification_stream(env, topics, CONNECTION_CHANNEL)
end end
end end

View File

@ -97,7 +97,7 @@ module Invidious::Routes::API::V1::Channels
json.field "autoGenerated", channel.auto_generated json.field "autoGenerated", channel.auto_generated
json.field "ageGated", channel.is_age_gated json.field "ageGated", channel.is_age_gated
json.field "isFamilyFriendly", channel.is_family_friendly json.field "isFamilyFriendly", channel.is_family_friendly
json.field "description", html_to_content(channel.description_html) json.field "description", Helpers.html_to_content(channel.description_html)
json.field "descriptionHtml", channel.description_html json.field "descriptionHtml", channel.description_html
json.field "allowedRegions", channel.allowed_regions json.field "allowedRegions", channel.allowed_regions
@ -128,7 +128,6 @@ module Invidious::Routes::API::V1::Channels
end end
end end
end # relatedChannels end # relatedChannels
end end
end end
end end

View File

@ -300,7 +300,7 @@ module Invidious::Routes::API::V1::Videos
annotations = response.body annotations = response.body
cache_annotation(id, annotations) Helpers.cache_annotation(id, annotations)
end end
else # "youtube" else # "youtube"
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}") response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")

View File

@ -19,7 +19,7 @@ module Invidious::Routes::BeforeAll
preferences = Preferences.from_json(URI.decode_www_form(prefs_cookie.value)) preferences = Preferences.from_json(URI.decode_www_form(prefs_cookie.value))
else else
if language_header = env.request.headers["Accept-Language"]? if language_header = env.request.headers["Accept-Language"]?
if language = ANG.language_negotiator.best(language_header, LOCALES.keys) if language = ANG.language_negotiator.best(language_header, I18n::LOCALES.keys)
preferences.locale = language.header preferences.locale = language.header
end end
end end

View File

@ -354,7 +354,7 @@ module Invidious::Routes::Channels
resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}") resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}")
ucid = resolved_url["endpoint"]["browseEndpoint"]["browseId"] ucid = resolved_url["endpoint"]["browseEndpoint"]["browseId"]
rescue ex : InfoException | KeyError rescue ex : InfoException | KeyError
return error_template(404, translate(locale, "This channel does not exist.")) return error_template(404, I18n.translate(locale, "This channel does not exist."))
end end
selected_tab = env.params.url["tab"]? selected_tab = env.params.url["tab"]?

View File

@ -10,7 +10,7 @@ module Invidious::Routes::Embed
videos = get_playlist_videos(playlist, offset: offset) videos = get_playlist_videos(playlist, offset: offset)
if videos.empty? if videos.empty?
url = "/playlist?list=#{plid}" url = "/playlist?list=#{plid}"
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url)) raise NotFoundException.new(I18n.translate(locale, "error_video_not_in_playlist", url))
end end
first_playlist_video = videos[0].as(PlaylistVideo) first_playlist_video = videos[0].as(PlaylistVideo)
@ -71,7 +71,7 @@ module Invidious::Routes::Embed
videos = get_playlist_videos(playlist, offset: offset) videos = get_playlist_videos(playlist, offset: offset)
if videos.empty? if videos.empty?
url = "/playlist?list=#{plid}" url = "/playlist?list=#{plid}"
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url)) raise NotFoundException.new(I18n.translate(locale, "error_video_not_in_playlist", url))
end end
first_playlist_video = videos[0].as(PlaylistVideo) first_playlist_video = videos[0].as(PlaylistVideo)

View File

@ -33,7 +33,13 @@ module Invidious::Routes::Feeds
def self.popular(env) def self.popular(env)
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
templated "feeds/popular"
if CONFIG.popular_enabled
templated "feeds/popular"
else
message = I18n.translate(locale, "The Popular feed has been disabled by the administrator.")
templated "message"
end
end end
def self.trending(env) def self.trending(env)
@ -253,7 +259,7 @@ module Invidious::Routes::Feeds
xml.element("link", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions") xml.element("link", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions")
xml.element("link", "type": "application/atom+xml", rel: "self", xml.element("link", "type": "application/atom+xml", rel: "self",
href: "#{HOST_URL}#{env.request.resource}") href: "#{HOST_URL}#{env.request.resource}")
xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) } xml.element("title") { xml.text I18n.translate(locale, "Invidious Private Feed for `x`", user.email) }
(notifications + videos).each do |video| (notifications + videos).each do |video|
video.to_xml(locale, params, xml) video.to_xml(locale, params, xml)

View File

@ -96,7 +96,7 @@ module Invidious::Routes::Images
break break
end end
proxy_file(response, env) Helpers.proxy_file(response, env)
end end
rescue ex rescue ex
end end
@ -148,6 +148,6 @@ module Invidious::Routes::Images
return env.response.headers.delete("Transfer-Encoding") return env.response.headers.delete("Transfer-Encoding")
end end
return proxy_file(response, env) return Helpers.proxy_file(response, env)
end end
end end

View File

@ -112,7 +112,7 @@ module Invidious::Routes::Login
user, sid = create_user(sid, email, password) user, sid = create_user(sid, email, password)
if language_header = env.request.headers["Accept-Language"]? if language_header = env.request.headers["Accept-Language"]?
if language = ANG.language_negotiator.best(language_header, LOCALES.keys) if language = ANG.language_negotiator.best(language_header, I18n::LOCALES.keys)
user.preferences.locale = language.header user.preferences.locale = language.header
end end
end end

View File

@ -83,7 +83,7 @@ module Invidious::Routes::VideoPlayback
# Remove the Range header added previously. # Remove the Range header added previously.
headers.delete("Range") if range_header.nil? headers.delete("Range") if range_header.nil?
playback_statistics = get_playback_statistic() playback_statistics = Helpers.get_playback_statistic
playback_statistics["totalRequests"] += 1 playback_statistics["totalRequests"] += 1
if response.status_code >= 400 if response.status_code >= 400
@ -195,7 +195,7 @@ module Invidious::Routes::VideoPlayback
end end
end end
proxy_file(resp, env) Helpers.proxy_file(resp, env)
end end
rescue ex rescue ex
if ex.message != "Error reading socket: Connection reset by peer" if ex.message != "Error reading socket: Connection reset by peer"

View File

@ -21,7 +21,7 @@ module Invidious::Search
if response.status_code == 404 if response.status_code == 404
response = YT_POOL.client &.get("/user/#{query.channel}") response = YT_POOL.client &.get("/user/#{query.channel}")
response = YT_POOL.client &.get("/c/#{query.channel}") if response.status_code == 404 response = YT_POOL.client &.get("/c/#{query.channel}") if response.status_code == 404
initial_data = extract_initial_data(response.body) initial_data = Helpers.extract_initial_data(response.body)
ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?) ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?)
raise ChannelSearchException.new(query.channel) if !ucid raise ChannelSearchException.new(query.channel) if !ucid
else else

View File

@ -15,7 +15,7 @@ struct Invidious::User
playlists.each do |playlist| playlists.each do |playlist|
json.object do json.object do
json.field "title", playlist.title json.field "title", playlist.title
json.field "description", html_to_content(playlist.description_html) json.field "description", Helpers.html_to_content(playlist.description_html)
json.field "privacy", playlist.privacy.to_s json.field "privacy", playlist.privacy.to_s
json.field "videos" do json.field "videos" do
json.array do json.array do

View File

@ -84,7 +84,7 @@ def extract_video_info(video_id : String)
# Although technically not a call to /videoplayback the fact that YouTube is returning the # Although technically not a call to /videoplayback the fact that YouTube is returning the
# wrong video means that we should count it as a failure. # wrong video means that we should count it as a failure.
get_playback_statistic()["totalRequests"] += 1 Helpers.get_playback_statistic["totalRequests"] += 1
return { return {
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),

View File

@ -8,12 +8,12 @@
<div class="pure-u-1 pure-u-lg-3-5"> <div class="pure-u-1 pure-u-lg-3-5">
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/add_playlist_items" method="get"> <form class="pure-form pure-form-aligned" action="/add_playlist_items" method="get">
<legend><a href="/playlist?list=<%= playlist.id %>"><%= translate(locale, "Editing playlist `x`", %|"#{HTML.escape(playlist.title)}"|) %></a></legend> <legend><a href="/playlist?list=<%= playlist.id %>"><%= I18n.translate(locale, "Editing playlist `x`", %|"#{HTML.escape(playlist.title)}"|) %></a></legend>
<fieldset> <fieldset>
<input class="pure-input-1" type="search" name="q" <input class="pure-input-1" type="search" name="q"
<% if query %>value="<%= HTML.escape(query.text) %>"<% end %> <% if query %>value="<%= HTML.escape(query.text) %>"<% end %>
placeholder="<%= translate(locale, "Search for videos") %>"> placeholder="<%= I18n.translate(locale, "Search for videos") %>">
<input type="hidden" name="list" value="<%= plid %>"> <input type="hidden" name="list" value="<%= plid %>">
</fieldset> </fieldset>
</form> </form>

View File

@ -35,10 +35,10 @@
<%= <%=
{ {
"ucid" => ucid, "ucid" => ucid,
"youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")), "youtube_comments_text" => HTML.escape(I18n.translate(locale, "View YouTube comments")),
"comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")), "comments_text" => HTML.escape(I18n.translate(locale, "View `x` comments", "{commentCount}")),
"hide_replies_text" => HTML.escape(translate(locale, "Hide replies")), "hide_replies_text" => HTML.escape(I18n.translate(locale, "Hide replies")),
"show_replies_text" => HTML.escape(translate(locale, "Show replies")), "show_replies_text" => HTML.escape(I18n.translate(locale, "Show replies")),
"preferences" => env.get("preferences").as(Preferences) "preferences" => env.get("preferences").as(Preferences)
}.to_pretty_json }.to_pretty_json
%> %>

View File

@ -27,7 +27,7 @@
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary" dir="auto" href="/feed/channel/<%= ucid %>"> <a class="pure-button pure-button-secondary" dir="auto" href="/feed/channel/<%= ucid %>">
<i class="icon ion-logo-rss"></i>&nbsp;<%= translate(locale, "generic_button_rss") %> <i class="icon ion-logo-rss"></i>&nbsp;<%= I18n.translate(locale, "generic_button_rss") %>
</a> </a>
</div> </div>
</div> </div>
@ -40,10 +40,10 @@
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<div class="pure-u-1 pure-md-1-3"> <div class="pure-u-1 pure-md-1-3">
<a href="<%= youtube_url %>"><%= translate(locale, "View channel on YouTube") %></a> <a href="<%= youtube_url %>"><%= I18n.translate(locale, "View channel on YouTube") %></a>
</div> </div>
<div class="pure-u-1 pure-md-1-3"> <div class="pure-u-1 pure-md-1-3">
<a href="<%= redirect_url %>"><%= translate(locale, "Switch Invidious Instance") %></a> <a href="<%= redirect_url %>"><%= I18n.translate(locale, "Switch Invidious Instance") %></a>
</div> </div>
<%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, selected_tab) %> <%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, selected_tab) %>
@ -53,9 +53,9 @@
<% sort_options.each do |sort| %> <% sort_options.each do |sort| %>
<div class="pure-u-1 pure-md-1-3"> <div class="pure-u-1 pure-md-1-3">
<% if sort_by == sort %> <% if sort_by == sort %>
<b><%= translate(locale, sort) %></b> <b><%= I18n.translate(locale, sort) %></b>
<% else %> <% else %>
<a href="<%= relative_url %>?sort_by=<%= sort %>"><%= translate(locale, sort) %></a> <a href="<%= relative_url %>?sort_by=<%= sort %>"><%= I18n.translate(locale, sort) %></a>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>

View File

@ -17,7 +17,7 @@
end %> end %>
<% feed_menu.each do |feed| %> <% feed_menu.each do |feed| %>
<a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading"> <a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading">
<%= translate(locale, feed) %> <%= I18n.translate(locale, feed) %>
</a> </a>
<% end %> <% end %>
</div> </div>

View File

@ -27,8 +27,8 @@
</div> </div>
<% if !item.channel_handle.nil? %><p class="channel-name" dir="auto"><%= item.channel_handle %></p><% end %> <% if !item.channel_handle.nil? %><p class="channel-name" dir="auto"><%= item.channel_handle %></p><% end %>
<p><%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %></p> <p><%= I18n.translate_count(locale, "generic_subscribers_count", item.subscriber_count, I18n::NumberFormatting::Separator) %></p>
<% if !item.auto_generated && item.channel_handle.nil? %><p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p><% end %> <% if !item.auto_generated && item.channel_handle.nil? %><p><%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %></p><% end %>
<h5><%= item.description_html %></h5> <h5><%= item.description_html %></h5>
<% when SearchHashtag %> <% when SearchHashtag %>
<% if !thin_mode %> <% if !thin_mode %>
@ -45,13 +45,13 @@
<div class="video-card-row"> <div class="video-card-row">
<%- if item.video_count != 0 -%> <%- if item.video_count != 0 -%>
<p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p> <p><%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %></p>
<%- end -%> <%- end -%>
</div> </div>
<div class="video-card-row"> <div class="video-card-row">
<%- if item.channel_count != 0 -%> <%- if item.channel_count != 0 -%>
<p><%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %></p> <p><%= I18n.translate_count(locale, "generic_channels_count", item.channel_count, I18n::NumberFormatting::Separator) %></p>
<%- end -%> <%- end -%>
</div> </div>
<% when SearchPlaylist, InvidiousPlaylist %> <% when SearchPlaylist, InvidiousPlaylist %>
@ -73,7 +73,7 @@
<%- end -%> <%- end -%>
<div class="bottom-right-overlay"> <div class="bottom-right-overlay">
<p class="length"><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p> <p class="length"><%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %></p>
</div> </div>
</div> </div>
@ -101,11 +101,11 @@
<div class="error-card"> <div class="error-card">
<div class="explanation"> <div class="explanation">
<i class="icon ion-ios-alert"></i> <i class="icon ion-ios-alert"></i>
<h4><%=translate(locale, "timeline_parse_error_placeholder_heading")%></h4> <h4><%=I18n.translate(locale, "timeline_parse_error_placeholder_heading")%></h4>
<p><%=translate(locale, "timeline_parse_error_placeholder_message")%></p> <p><%=I18n.translate(locale, "timeline_parse_error_placeholder_message")%></p>
</div> </div>
<details> <details>
<summary class="pure-button pure-button-secondary"><%=translate(locale, "timeline_parse_error_show_technical_details")%></summary> <summary class="pure-button pure-button-secondary"><%=I18n.translate(locale, "timeline_parse_error_show_technical_details")%></summary>
<pre class="error-issue-template"><%=get_issue_template(env, item.parse_exception)[1]%></pre> <pre class="error-issue-template"><%=get_issue_template(env, item.parse_exception)[1]%></pre>
</details> </details>
</div> </div>
@ -168,7 +168,7 @@
<div class="bottom-right-overlay"> <div class="bottom-right-overlay">
<%- if item.responds_to?(:live_now) && item.live_now -%> <%- if item.responds_to?(:live_now) && item.live_now -%>
<p class="length" dir="auto"><i class="icon ion-ios-play-circle"></i>&nbsp;<%= translate(locale, "LIVE") %></p> <p class="length" dir="auto"><i class="icon ion-ios-play-circle"></i>&nbsp;<%= I18n.translate(locale, "LIVE") %></p>
<%- elsif item.length_seconds != 0 -%> <%- elsif item.length_seconds != 0 -%>
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p> <p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
<%- end -%> <%- end -%>
@ -200,15 +200,15 @@
<div class="video-card-row flexible"> <div class="video-card-row flexible">
<div class="flex-left"> <div class="flex-left">
<% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %> <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %>
<p class="video-data" dir="auto"><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %></p> <p class="video-data" dir="auto"><%= I18n.translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %></p>
<% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %> <% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %>
<p class="video-data" dir="auto"><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></p> <p class="video-data" dir="auto"><%= I18n.translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></p>
<% end %> <% end %>
</div> </div>
<% if item.responds_to?(:views) && item.views %> <% if item.responds_to?(:views) && item.views %>
<div class="flex-right"> <div class="flex-right">
<p class="video-data" dir="auto"><%= translate_count(locale, "generic_views_count", item.views || 0, NumberFormatting::Short) %></p> <p class="video-data" dir="auto"><%= I18n.translate_count(locale, "generic_views_count", item.views || 0, I18n::NumberFormatting::Short) %></p>
</div> </div>
<% end %> <% end %>
</div> </div>

View File

@ -11,9 +11,9 @@
<script id="pagination-data" type="application/json"> <script id="pagination-data" type="application/json">
<%= <%=
{ {
"next_page" => translate(locale, "Next page"), "next_page" => I18n.translate(locale, "Next page"),
"prev_page" => translate(locale, "Previous page"), "prev_page" => I18n.translate(locale, "Previous page"),
"is_rtl" => locale_is_rtl?(locale) "is_rtl" => I18n.locale_is_rtl?(locale)
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>

View File

@ -3,11 +3,11 @@
<% search_placeholder = CONFIG.page_enabled?("search") ? translate(locale, "search") : translate(locale, "search_subscriptions_placeholder") %> <% search_placeholder = CONFIG.page_enabled?("search") ? translate(locale, "search") : translate(locale, "search_subscriptions_placeholder") %>
<input type="search" id="searchbox" autocorrect="off" <input type="search" id="searchbox" autocorrect="off"
autocapitalize="none" spellcheck="false" <% if autofocus %>autofocus<% end %> autocapitalize="none" spellcheck="false" <% if autofocus %>autofocus<% end %>
name="q" placeholder="<%= search_placeholder %>" name="q" placeholder="<%= I18n.translate(locale, "search") %>"
title="<%= search_placeholder %>" title="<%= I18n.translate(locale, "search") %>"
value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>"> value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>">
</fieldset> </fieldset>
<button type="submit" id="searchbutton" aria-label="<%= search_placeholder %>"> <button type="submit" id="searchbutton" aria-label="<%= I18n.translate(locale, "search") %>">
<i class="icon ion-ios-search"></i> <i class="icon ion-ios-search"></i>
</button> </button>
</form> </form>

View File

@ -3,14 +3,14 @@
<form action="/subscription_ajax?action=remove_subscriptions&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post"> <form action="/subscription_ajax?action=remove_subscriptions&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button data-type="unsubscribe" id="subscribe" class="pure-button pure-button-primary"> <button data-type="unsubscribe" id="subscribe" class="pure-button pure-button-primary">
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b> <b><input style="all:unset" type="submit" value="<%= I18n.translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b>
</button> </button>
</form> </form>
<% else %> <% else %>
<form action="/subscription_ajax?action=create_subscription_to_channel&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post"> <form action="/subscription_ajax?action=create_subscription_to_channel&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button data-type="subscribe" id="subscribe" class="pure-button pure-button-primary"> <button data-type="subscribe" id="subscribe" class="pure-button pure-button-primary">
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b> <b><input style="all:unset" type="submit" value="<%= I18n.translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b>
</button> </button>
</form> </form>
<% end %> <% end %>
@ -22,8 +22,8 @@
"author" => HTML.escape(author), "author" => HTML.escape(author),
"sub_count_text" => HTML.escape(sub_count_text), "sub_count_text" => HTML.escape(sub_count_text),
"csrf_token" => URI.encode_www_form(env.get?("csrf_token").try &.as(String) || ""), "csrf_token" => URI.encode_www_form(env.get?("csrf_token").try &.as(String) || ""),
"subscribe_text" => HTML.escape(translate(locale, "Subscribe")), "subscribe_text" => HTML.escape(I18n.translate(locale, "Subscribe")),
"unsubscribe_text" => HTML.escape(translate(locale, "Unsubscribe")) "unsubscribe_text" => HTML.escape(I18n.translate(locale, "Unsubscribe"))
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
@ -31,6 +31,6 @@
<% else %> <% else %>
<a id="subscribe" class="pure-button pure-button-primary" <a id="subscribe" class="pure-button pure-button-primary"
href="/login?referer=<%= env.get("current_page") %>"> href="/login?referer=<%= env.get("current_page") %>">
<b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b> <b><%= I18n.translate(locale, "Subscribe") %> | <%= sub_count_text %></b>
</a> </a>
<% end %> <% end %>

View File

@ -1,18 +1,18 @@
<div class="flex-right flexible"> <div class="flex-right flexible">
<div class="icon-buttons"> <div class="icon-buttons">
<a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>"> <a title="<%=I18n.translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>">
<i class="icon ion-logo-youtube"></i> <i class="icon ion-logo-youtube"></i>
</a> </a>
<a title="<%=translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1"> <a title="<%=I18n.translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1">
<i class="icon ion-md-headset"></i> <i class="icon ion-md-headset"></i>
</a> </a>
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%> <% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
<a title="<%=translate(locale, "Switch Invidious Instance")%>" href="/redirect?referer=%2Fwatch<%=URI.encode_www_form(endpoint_params)%>"> <a title="<%=I18n.translate(locale, "Switch Invidious Instance")%>" href="/redirect?referer=%2Fwatch<%=URI.encode_www_form(endpoint_params)%>">
<i class="icon ion-md-jet"></i> <i class="icon ion-md-jet"></i>
</a> </a>
<% else %> <% else %>
<a title="<%=translate(locale, "Switch Invidious Instance")%>" href="https://redirect.invidious.io/watch<%=endpoint_params%>"> <a title="<%=I18n.translate(locale, "Switch Invidious Instance")%>" href="https://redirect.invidious.io/watch<%=endpoint_params%>">
<i class="icon ion-md-jet"></i> <i class="icon ion-md-jet"></i>
</a> </a>
<% end %> <% end %>

View File

@ -1,5 +1,5 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Create playlist") %> - Invidious</title> <title><%= I18n.translate(locale, "Create playlist") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g"> <div class="pure-g">
@ -8,25 +8,25 @@
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/create_playlist?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/create_playlist?referer=<%= URI.encode_www_form(referer) %>" method="post">
<fieldset> <fieldset>
<legend><%= translate(locale, "Create playlist") %></legend> <legend><%= I18n.translate(locale, "Create playlist") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="title"><%= translate(locale, "Title") %> :</label> <label for="title"><%= I18n.translate(locale, "Title") %> :</label>
<input required name="title" type="text" placeholder="<%= translate(locale, "Title") %>"> <input required name="title" type="text" placeholder="<%= I18n.translate(locale, "Title") %>">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="privacy"><%= translate(locale, "Playlist privacy") %> :</label> <label for="privacy"><%= I18n.translate(locale, "Playlist privacy") %> :</label>
<select name="privacy" id="privacy"> <select name="privacy" id="privacy">
<% PlaylistPrivacy.names.each do |option| %> <% PlaylistPrivacy.names.each do |option| %>
<option value="<%= option %>" <% if option == "Public" %> selected <% end %>><%= translate(locale, option) %></option> <option value="<%= option %>" <% if option == "Public" %> selected <% end %>><%= I18n.translate(locale, option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-controls"> <div class="pure-controls">
<button type="submit" name="action" value="create_playlist" class="pure-button pure-button-primary"> <button type="submit" name="action" value="create_playlist" class="pure-button pure-button-primary">
<%= translate(locale, "Create playlist") %> <%= I18n.translate(locale, "Create playlist") %>
</button> </button>
</div> </div>

View File

@ -1,20 +1,20 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Delete playlist") %> - Invidious</title> <title><%= I18n.translate(locale, "Delete playlist") %> - Invidious</title>
<% end %> <% end %>
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/delete_playlist?list=<%= plid %>&referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/delete_playlist?list=<%= plid %>&referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= translate(locale, "Delete playlist `x`?", %|"#{HTML.escape(playlist.title)}"|) %></legend> <legend><%= I18n.translate(locale, "Delete playlist `x`?", %|"#{HTML.escape(playlist.title)}"|) %></legend>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<button type="submit" name="submit" value="delete_playlist" class="pure-button pure-button-primary"> <button type="submit" name="submit" value="delete_playlist" class="pure-button pure-button-primary">
<%= translate(locale, "Yes") %> <%= I18n.translate(locale, "Yes") %>
</button> </button>
</div> </div>
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<a class="pure-button" href="/playlist?list=<%= plid %>"> <a class="pure-button" href="/playlist?list=<%= plid %>">
<%= translate(locale, "No") %> <%= I18n.translate(locale, "No") %>
</a> </a>
</div> </div>
</div> </div>

View File

@ -10,17 +10,17 @@
<div class="flex-right button-container"> <div class="flex-right button-container">
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/playlist?list=<%= plid %>">
<i class="icon ion-md-close"></i>&nbsp;<%= translate(locale, "generic_button_cancel") %> <i class="icon ion-md-close"></i>&nbsp;<%= I18n.translate(locale, "generic_button_cancel") %>
</a> </a>
</div> </div>
<div class="pure-u"> <div class="pure-u">
<button class="pure-button pure-button-secondary low-profile" dir="auto" type="submit"> <button class="pure-button pure-button-secondary low-profile" dir="auto" type="submit">
<i class="icon ion-md-save"></i>&nbsp;<%= translate(locale, "generic_button_save") %> <i class="icon ion-md-save"></i>&nbsp;<%= I18n.translate(locale, "generic_button_save") %>
</button> </button>
</div> </div>
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>">
<i class="icon ion-md-trash"></i>&nbsp;<%= translate(locale, "generic_button_delete") %> <i class="icon ion-md-trash"></i>&nbsp;<%= I18n.translate(locale, "generic_button_delete") %>
</a> </a>
</div> </div>
</div> </div>
@ -36,11 +36,11 @@
<div class="pure-u-1-1"> <div class="pure-u-1-1">
<b> <b>
<%= HTML.escape(playlist.author) %> | <%= HTML.escape(playlist.author) %> |
<%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | <%= I18n.translate_count(locale, "generic_videos_count", playlist.video_count) %> |
</b> </b>
<select name="privacy"> <select name="privacy">
<%- {"Public", "Unlisted", "Private"}.each do |option| -%> <%- {"Public", "Unlisted", "Private"}.each do |option| -%>
<option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= translate(locale, option) %></option> <option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= I18n.translate(locale, option) %></option>
<%- end -%> <%- end -%>
</select> </select>
</div> </div>

View File

@ -1,19 +1,19 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "History") %> - Invidious</title> <title><%= I18n.translate(locale, "History") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3><%= translate_count(locale, "generic_videos_count", user.watched.size, NumberFormatting::HtmlSpan) %></h3> <h3><%= I18n.translate_count(locale, "generic_videos_count", user.watched.size, I18n::NumberFormatting::HtmlSpan) %></h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:center"> <h3 style="text-align:center">
<a href="/feed/subscriptions"><%= translate_count(locale, "generic_subscriptions_count", user.subscriptions.size, NumberFormatting::HtmlSpan) %></a> <a href="/feed/subscriptions"><%= I18n.translate_count(locale, "generic_subscriptions_count", user.subscriptions.size, I18n::NumberFormatting::HtmlSpan) %></a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:right"> <h3 style="text-align:right">
<a href="/clear_watch_history"><%= translate(locale, "Clear watch history") %></a> <a href="/clear_watch_history"><%= I18n.translate(locale, "Clear watch history") %></a>
</h3> </h3>
</div> </div>
</div> </div>

View File

@ -1,22 +1,22 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Playlists") %> - Invidious</title> <title><%= I18n.translate(locale, "Playlists") %> - Invidious</title>
<% end %> <% end %>
<%= rendered "components/feed_menu" %> <%= rendered "components/feed_menu" %>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3><%= translate(locale, "user_created_playlists", %(<span id="count">#{items_created.size}</span>)) %></h3> <h3><%= I18n.translate(locale, "user_created_playlists", %(<span id="count">#{items_created.size}</span>)) %></h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:center"> <h3 style="text-align:center">
<a href="/create_playlist?referer=<%= URI.encode_www_form("/feed/playlists") %>"><%= translate(locale, "Create playlist") %></a> <a href="/create_playlist?referer=<%= URI.encode_www_form("/feed/playlists") %>"><%= I18n.translate(locale, "Create playlist") %></a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:right"> <h3 style="text-align:right">
<a href="/data_control?referer=<%= URI.encode_www_form("/feed/playlists") %>"> <a href="/data_control?referer=<%= URI.encode_www_form("/feed/playlists") %>">
<%= translate(locale, "Import/export") %> <%= I18n.translate(locale, "Import/export") %>
</a> </a>
</h3> </h3>
</div> </div>
@ -30,7 +30,7 @@
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1"> <div class="pure-u-1">
<h3><%= translate(locale, "user_saved_playlists", %(<span id="count">#{items_saved.size}</span>)) %></h3> <h3><%= I18n.translate(locale, "user_saved_playlists", %(<span id="count">#{items_saved.size}</span>)) %></h3>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> <meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>">
<title> <title>
<% if env.get("preferences").as(Preferences).default_home != "Popular" %> <% if env.get("preferences").as(Preferences).default_home != "Popular" %>
<%= translate(locale, "Popular") %> - Invidious <%= I18n.translate(locale, "Popular") %> - Invidious
<% else %> <% else %>
Invidious Invidious
<% end %> <% end %>

View File

@ -1,5 +1,5 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Subscriptions") %> - Invidious</title> <title><%= I18n.translate(locale, "Subscriptions") %> - Invidious</title>
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/private?token=<%= token %>" /> <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/private?token=<%= token %>" />
<% end %> <% end %>
@ -8,12 +8,12 @@
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3> <h3>
<a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a> <a href="/subscription_manager"><%= I18n.translate(locale, "Manage subscriptions") %></a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:center"> <h3 style="text-align:center">
<a href="/feed/history"><%= translate(locale, "Watch history") %></a> <a href="/feed/history"><%= I18n.translate(locale, "Watch history") %></a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
@ -26,7 +26,7 @@
<% if CONFIG.enable_user_notifications %> <% if CONFIG.enable_user_notifications %>
<center> <center>
<%= translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %> <%= I18n.translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %>
</center> </center>
<% if !notifications.empty? %> <% if !notifications.empty? %>

View File

@ -1,8 +1,8 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> <meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>">
<title> <title>
<% if env.get("preferences").as(Preferences).default_home != "Trending" %> <% if env.get("preferences").as(Preferences).default_home != "Trending" %>
<%= translate(locale, "Trending") %> - Invidious <%= I18n.translate(locale, "Trending") %> - Invidious
<% else %> <% else %>
Invidious Invidious
<% end %> <% end %>
@ -15,7 +15,7 @@
<div style="align-self:flex-end" class="pure-u-2-3"> <div style="align-self:flex-end" class="pure-u-2-3">
<% if plid %> <% if plid %>
<a href="/playlist?list=<%= plid %>"> <a href="/playlist?list=<%= plid %>">
<%= translate(locale, "View as playlist") %> <%= I18n.translate(locale, "View as playlist") %>
</a> </a>
<% end %> <% end %>
</div> </div>
@ -24,10 +24,10 @@
<% {"Livestreams", "Gaming"}.each do |option| %> <% {"Livestreams", "Gaming"}.each do |option| %>
<div class="pure-u-1 pure-md-1-3"> <div class="pure-u-1 pure-md-1-3">
<% if trending_type == option %> <% if trending_type == option %>
<b><%= translate(locale, option) %></b> <b><%= I18n.translate(locale, option) %></b>
<% else %> <% else %>
<a href="/feed/trending?type=<%= option %>&region=<%= region %>"> <a href="/feed/trending?type=<%= option %>&region=<%= region %>">
<%= translate(locale, option) %> <%= I18n.translate(locale, option) %>
</a> </a>
<% end %> <% end %>
</div> </div>

View File

@ -7,7 +7,7 @@
</head> </head>
<body> <body>
<h1><%= translate(locale, "JavaScript license information") %></h1> <h1><%= I18n.translate(locale, "JavaScript license information") %></h1>
<table id="jslicense-labels1"> <table id="jslicense-labels1">
<tr> <tr>
<td> <td>
@ -19,7 +19,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/iv-org/videojs-quality-selector"><%= translate(locale, "source") %></a> <a href="https://github.com/iv-org/videojs-quality-selector"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -33,7 +33,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/mpetazzoni/sse.js"><%= translate(locale, "source") %></a> <a href="https://github.com/mpetazzoni/sse.js"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -47,7 +47,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/videojs/videojs-contrib-quality-levels"><%= translate(locale, "source") %></a> <a href="https://github.com/videojs/videojs-contrib-quality-levels"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -61,7 +61,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/jfujita/videojs-http-source-selector"><%= translate(locale, "source") %></a> <a href="https://github.com/jfujita/videojs-http-source-selector"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -75,7 +75,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/mister-ben/videojs-mobile-ui"><%= translate(locale, "source") %></a> <a href="https://github.com/mister-ben/videojs-mobile-ui"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -89,7 +89,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/spchuang/videojs-markers"><%= translate(locale, "source") %></a> <a href="https://github.com/spchuang/videojs-markers"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -103,7 +103,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/brightcove/videojs-overlay"><%= translate(locale, "source") %></a> <a href="https://github.com/brightcove/videojs-overlay"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -117,7 +117,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/mkhazov/videojs-share"><%= translate(locale, "source") %></a> <a href="https://github.com/mkhazov/videojs-share"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -131,7 +131,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/chrisboustead/videojs-vtt-thumbnails"><%= translate(locale, "source") %></a> <a href="https://github.com/chrisboustead/videojs-vtt-thumbnails"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -145,7 +145,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/afrmtbl/videojs-youtube-annotations"><%= translate(locale, "source") %></a> <a href="https://github.com/afrmtbl/videojs-youtube-annotations"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -159,7 +159,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/videojs/videojs-vr"><%= translate(locale, "source") %></a> <a href="https://github.com/videojs/videojs-vr"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>
@ -173,7 +173,7 @@
</td> </td>
<td> <td>
<a href="https://github.com/videojs/video.js"><%= translate(locale, "source") %></a> <a href="https://github.com/videojs/video.js"><%= I18n.translate(locale, "source") %></a>
</td> </td>
</tr> </tr>

View File

@ -1,5 +1,5 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> <meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>">
<title> <title>
Invidious Invidious
</title> </title>

View File

@ -13,28 +13,28 @@
<%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%> <%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%>
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/add_playlist_items?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/add_playlist_items?list=<%= plid %>">
<i class="icon ion-md-add"></i>&nbsp;<%= translate(locale, "playlist_button_add_items") %> <i class="icon ion-md-add"></i>&nbsp;<%= I18n.translate(locale, "playlist_button_add_items") %>
</a> </a>
</div> </div>
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/edit_playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/edit_playlist?list=<%= plid %>">
<i class="icon ion-md-create"></i>&nbsp;<%= translate(locale, "generic_button_edit") %> <i class="icon ion-md-create"></i>&nbsp;<%= I18n.translate(locale, "generic_button_edit") %>
</a> </a>
</div> </div>
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>">
<i class="icon ion-md-trash"></i>&nbsp;<%= translate(locale, "generic_button_delete") %> <i class="icon ion-md-trash"></i>&nbsp;<%= I18n.translate(locale, "generic_button_delete") %>
</a> </a>
</div> </div>
<%- else -%> <%- else -%>
<div class="pure-u"> <div class="pure-u">
<%- if IV::Database::Playlists.exists?(playlist.id) -%> <%- if IV::Database::Playlists.exists?(playlist.id) -%>
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/subscribe_playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/subscribe_playlist?list=<%= plid %>">
<i class="icon ion-md-add"></i>&nbsp;<%= translate(locale, "Subscribe") %> <i class="icon ion-md-add"></i>&nbsp;<%= I18n.translate(locale, "Subscribe") %>
</a> </a>
<%- else -%> <%- else -%>
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>">
<i class="icon ion-md-trash"></i>&nbsp;<%= translate(locale, "Unsubscribe") %> <i class="icon ion-md-trash"></i>&nbsp;<%= I18n.translate(locale, "Unsubscribe") %>
</a> </a>
<%- end -%> <%- end -%>
</div> </div>
@ -42,7 +42,7 @@
<div class="pure-u"> <div class="pure-u">
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/feed/playlist/<%= plid %>"> <a class="pure-button pure-button-secondary low-profile" dir="auto" href="/feed/playlist/<%= plid %>">
<i class="icon ion-logo-rss"></i>&nbsp;<%= translate(locale, "generic_button_rss") %> <i class="icon ion-logo-rss"></i>&nbsp;<%= I18n.translate(locale, "generic_button_rss") %>
</a> </a>
</div> </div>
</div> </div>
@ -57,15 +57,15 @@
<% else %> <% else %>
<%= author %> | <%= author %> |
<% end %> <% end %>
<%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | <%= I18n.translate_count(locale, "generic_videos_count", playlist.video_count) %> |
<%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> | <%= I18n.translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> |
<% case playlist.as(InvidiousPlaylist).privacy when %> <% case playlist.as(InvidiousPlaylist).privacy when %>
<% when PlaylistPrivacy::Public %> <% when PlaylistPrivacy::Public %>
<i class="icon ion-md-globe"></i> <%= translate(locale, "Public") %> <i class="icon ion-md-globe"></i> <%= I18n.translate(locale, "Public") %>
<% when PlaylistPrivacy::Unlisted %> <% when PlaylistPrivacy::Unlisted %>
<i class="icon ion-ios-unlock"></i> <%= translate(locale, "Unlisted") %> <i class="icon ion-ios-unlock"></i> <%= I18n.translate(locale, "Unlisted") %>
<% when PlaylistPrivacy::Private %> <% when PlaylistPrivacy::Private %>
<i class="icon ion-ios-lock"></i> <%= translate(locale, "Private") %> <i class="icon ion-ios-lock"></i> <%= I18n.translate(locale, "Private") %>
<% end %> <% end %>
</b> </b>
<% else %> <% else %>
@ -76,25 +76,25 @@
<% subtitle = playlist.subtitle || "" %> <% subtitle = playlist.subtitle || "" %>
<span><%= HTML.escape(subtitle[0..subtitle.rindex(" • ") || subtitle.size]) %></span> | <span><%= HTML.escape(subtitle[0..subtitle.rindex(" • ") || subtitle.size]) %></span> |
<% end %> <% end %>
<%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | <%= I18n.translate_count(locale, "generic_videos_count", playlist.video_count) %> |
<%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> <%= I18n.translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %>
</b> </b>
<% end %> <% end %>
<% if !playlist.is_a? InvidiousPlaylist %> <% if !playlist.is_a? InvidiousPlaylist %>
<div class="pure-u-2-3"> <div class="pure-u-2-3">
<a rel="noreferrer noopener" href="https://www.youtube.com/playlist?list=<%= playlist.id %>"> <a rel="noreferrer noopener" href="https://www.youtube.com/playlist?list=<%= playlist.id %>">
<%= translate(locale, "View playlist on YouTube") %> <%= I18n.translate(locale, "View playlist on YouTube") %>
</a> </a>
<span> | </span> <span> | </span>
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%> <% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
<a href="/redirect?referer=<%= env.get?("current_page") %>"> <a href="/redirect?referer=<%= env.get?("current_page") %>">
<%= translate(locale, "Switch Invidious Instance") %> <%= I18n.translate(locale, "Switch Invidious Instance") %>
</a> </a>
<% else %> <% else %>
<a href="https://redirect.invidious.io/playlist?list=<%= playlist.id %>"> <a href="https://redirect.invidious.io/playlist?list=<%= playlist.id %>">
<%= translate(locale, "Switch Invidious Instance") %> <%= I18n.translate(locale, "Switch Invidious Instance") %>
</a> </a>
<% end %> <% end %>
</div> </div>

View File

@ -18,7 +18,7 @@
<% else %> <% else %>
<noscript> <noscript>
<a href="/post/<%= id %>?ucid=<%= ucid %>&nojs=1"> <a href="/post/<%= id %>?ucid=<%= ucid %>&nojs=1">
<%= translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %> <%= I18n.translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %>
</a> </a>
</noscript> </noscript>
<% end %> <% end %>
@ -29,12 +29,12 @@
<%= <%=
{ {
"id" => id, "id" => id,
"youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")), "youtube_comments_text" => HTML.escape(I18n.translate(locale, "View YouTube comments")),
"reddit_comments_text" => "", "reddit_comments_text" => "",
"reddit_permalink_text" => "", "reddit_permalink_text" => "",
"comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")), "comments_text" => HTML.escape(I18n.translate(locale, "View `x` comments", "{commentCount}")),
"hide_replies_text" => HTML.escape(translate(locale, "Hide replies")), "hide_replies_text" => HTML.escape(I18n.translate(locale, "Hide replies")),
"show_replies_text" => HTML.escape(translate(locale, "Show replies")), "show_replies_text" => HTML.escape(I18n.translate(locale, "Show replies")),
"params" => { "params" => {
"comments": ["youtube"] "comments": ["youtube"]
}, },

View File

@ -20,14 +20,9 @@
<%- if items.empty? -%> <%- if items.empty? -%>
<div class="h-box no-results-error"> <div class="h-box no-results-error">
<div> <div>
<% if subscription_only %> <%= I18n.translate(locale, "search_message_no_results") %><br/><br/>
<%= translate(locale, "search_subscriptions_no_results") %><br/><br/> <%= I18n.translate(locale, "search_message_change_filters_or_query") %><br/><br/>
<%= translate(locale, "search_subscriptions_no_results_hint") %> <%= I18n.translate(locale, "search_message_use_another_instance", redirect_url) %>
<% else %>
<%= translate(locale, "search_message_no_results") %><br/><br/>
<%= translate(locale, "search_message_change_filters_or_query") %><br/><br/>
<%= translate(locale, "search_message_use_another_instance", redirect_url) %>
<% end %>
</div> </div>
</div> </div>
<%- else -%> <%- else -%>

View File

@ -1,7 +1,7 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> <meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>">
<title> <title>
Invidious - <%= translate(locale, "search") %> Invidious - <%= I18n.translate(locale, "search") %>
</title> </title>
<link rel="stylesheet" href="/css/empty.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/empty.css?v=<%= ASSET_COMMIT %>">
<% end %> <% end %>

View File

@ -43,7 +43,7 @@
<div class="pure-u-1 pure-u-md-8-24 user-field"> <div class="pure-u-1 pure-u-md-8-24 user-field">
<% if env.get? "user" %> <% if env.get? "user" %>
<div class="pure-u-1-4"> <div class="pure-u-1-4">
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>"> <a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= I18n.translate(locale, "toggle_theme") %>">
<% if dark_mode == "dark" %> <% if dark_mode == "dark" %>
<i class="icon ion-ios-sunny"></i> <i class="icon ion-ios-sunny"></i>
<% else %> <% else %>
@ -52,7 +52,7 @@
</a> </a>
</div> </div>
<div class="pure-u-1-4"> <div class="pure-u-1-4">
<a id="notification_ticker" title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading"> <a id="notification_ticker" title="<%= I18n.translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
<% notification_count = env.get("user").as(Invidious::User).notifications.size %> <% notification_count = env.get("user").as(Invidious::User).notifications.size %>
<% if CONFIG.enable_user_notifications && notification_count > 0 %> <% if CONFIG.enable_user_notifications && notification_count > 0 %>
<span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i> <span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i>
@ -62,7 +62,7 @@
</a> </a>
</div> </div>
<div class="pure-u-1-4"> <div class="pure-u-1-4">
<a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> <a title="<%= I18n.translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
<i class="icon ion-ios-cog"></i> <i class="icon ion-ios-cog"></i>
</a> </a>
</div> </div>
@ -75,13 +75,13 @@
<form action="/signout?referer=<%= env.get?("current_page") %>" method="post"> <form action="/signout?referer=<%= env.get?("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<a class="pure-menu-heading" href="#"> <a class="pure-menu-heading" href="#">
<input style="all:unset" type="submit" value="<%= translate(locale, "Log out") %>"> <input style="all:unset" type="submit" value="<%= I18n.translate(locale, "Log out") %>">
</a> </a>
</form> </form>
</div> </div>
<% else %> <% else %>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>"> <a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= I18n.translate(locale, "toggle_theme") %>">
<% if dark_mode == "dark" %> <% if dark_mode == "dark" %>
<i class="icon ion-ios-sunny"></i> <i class="icon ion-ios-sunny"></i>
<% else %> <% else %>
@ -90,14 +90,14 @@
</a> </a>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> <a title="<%= I18n.translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
<i class="icon ion-ios-cog"></i> <i class="icon ion-ios-cog"></i>
</a> </a>
</div> </div>
<% if CONFIG.login_enabled %> <% if CONFIG.login_enabled %>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> <a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
<%= translate(locale, "Log in") %> <%= I18n.translate(locale, "Log in") %>
</a> </a>
</div> </div>
<% end %> <% end %>
@ -119,39 +119,39 @@
<span> <span>
<i class="icon ion-logo-github"></i> <i class="icon ion-logo-github"></i>
<% if CONFIG.modified_source_code_url %> <% if CONFIG.modified_source_code_url %>
<a href="https://github.com/iv-org/invidious"><%= translate(locale, "footer_original_source_code") %></a>&nbsp;/ <a href="https://github.com/iv-org/invidious"><%= I18n.translate(locale, "footer_original_source_code") %></a>&nbsp;/
<a href="<%= CONFIG.modified_source_code_url %>"><%= translate(locale, "footer_modfied_source_code") %></a> <a href="<%= CONFIG.modified_source_code_url %>"><%= I18n.translate(locale, "footer_modfied_source_code") %></a>
<% else %> <% else %>
<a href="https://github.com/iv-org/invidious"><%= translate(locale, "footer_source_code") %></a> <a href="https://github.com/iv-org/invidious"><%= I18n.translate(locale, "footer_source_code") %></a>
<% end %> <% end %>
</span> </span>
<span> <span>
<i class="icon ion-ios-paper"></i> <i class="icon ion-ios-paper"></i>
<a href="https://github.com/iv-org/documentation"><%= translate(locale, "footer_documentation") %></a> <a href="https://github.com/iv-org/documentation"><%= I18n.translate(locale, "footer_documentation") %></a>
</span> </span>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 pure-u-md-1-3">
<span> <span>
<a href="https://github.com/iv-org/invidious/blob/master/LICENSE"><%= translate(locale, "Released under the AGPLv3 on Github.") %></a> <a href="https://github.com/iv-org/invidious/blob/master/LICENSE"><%= I18n.translate(locale, "Released under the AGPLv3 on Github.") %></a>
</span> </span>
<span> <span>
<i class="icon ion-logo-javascript"></i> <i class="icon ion-logo-javascript"></i>
<a rel="jslicense" href="/licenses"><%= translate(locale, "View JavaScript license information.") %></a> <a rel="jslicense" href="/licenses"><%= I18n.translate(locale, "View JavaScript license information.") %></a>
</span> </span>
<span> <span>
<i class="icon ion-ios-paper"></i> <i class="icon ion-ios-paper"></i>
<a href="/privacy"><%= translate(locale, "View privacy policy.") %></a> <a href="/privacy"><%= I18n.translate(locale, "View privacy policy.") %></a>
</span> </span>
</div> </div>
<div class="pure-u-1 pure-u-md-1-3"> <div class="pure-u-1 pure-u-md-1-3">
<span> <span>
<i class="icon ion-ios-wallet"></i> <i class="icon ion-ios-wallet"></i>
<a href="https://invidious.io/donate/"><%= translate(locale, "footer_donate_page") %></a> <a href="https://invidious.io/donate/"><%= I18n.translate(locale, "footer_donate_page") %></a>
</span> </span>
<span> <span>
<%= translate(locale, "Current version: ") %> <%= I18n.translate(locale, "Current version: ") %>
<% if CONFIG.modified_source_code_url %> <% if CONFIG.modified_source_code_url %>
<a href="<%= CONFIG.modified_source_code_url %>/commit/<%= CURRENT_COMMIT %>"><%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %></a> <a href="<%= CONFIG.modified_source_code_url %>/commit/<%= CURRENT_COMMIT %>"><%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %></a>
<% else %> <% else %>
@ -181,8 +181,8 @@
<script id="notification_data" type="application/json"> <script id="notification_data" type="application/json">
<%= <%=
{ {
"upload_text" => HTML.escape(translate(locale, "`x` uploaded a video")), "upload_text" => HTML.escape(I18n.translate(locale, "`x` uploaded a video")),
"live_upload_text" => HTML.escape(translate(locale, "`x` is live")) "live_upload_text" => HTML.escape(I18n.translate(locale, "`x` is live"))
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>

View File

@ -1,22 +1,22 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Token") %> - Invidious</title> <title><%= I18n.translate(locale, "Token") %> - Invidious</title>
<% end %> <% end %>
<% if env.get? "access_token" %> <% if env.get? "access_token" %>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3> <h3>
<%= translate(locale, "Token") %> <%= I18n.translate(locale, "Token") %>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:center"> <h3 style="text-align:center">
<a href="/token_manager"><%= translate(locale, "Token manager") %></a> <a href="/token_manager"><%= I18n.translate(locale, "Token manager") %></a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:right"> <h3 style="text-align:right">
<a href="/preferences"><%= translate(locale, "Preferences") %></a> <a href="/preferences"><%= I18n.translate(locale, "Preferences") %></a>
</h3> </h3>
</div> </div>
</div> </div>
@ -30,9 +30,9 @@
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/authorize_token" method="post"> <form class="pure-form pure-form-aligned" action="/authorize_token" method="post">
<% if callback_url %> <% if callback_url %>
<legend><%= translate(locale, "Authorize token for `x`?", "#{callback_url.scheme}://#{callback_url.host}") %></legend> <legend><%= I18n.translate(locale, "Authorize token for `x`?", "#{callback_url.scheme}://#{callback_url.host}") %></legend>
<% else %> <% else %>
<legend><%= translate(locale, "Authorize token?") %></legend> <legend><%= I18n.translate(locale, "Authorize token?") %></legend>
<% end %> <% end %>
<div class="pure-g"> <div class="pure-g">
@ -48,7 +48,7 @@
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary"> <button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary">
<%= translate(locale, "Yes") %> <%= I18n.translate(locale, "Yes") %>
</button> </button>
</div> </div>
<div class="pure-u-1-2"> <div class="pure-u-1-2">
@ -57,7 +57,7 @@
<% else %> <% else %>
<a class="pure-button" href="/"> <a class="pure-button" href="/">
<% end %> <% end %>
<%= translate(locale, "No") %> <%= I18n.translate(locale, "No") %>
</a> </a>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Change password") %> - Invidious</title> <title><%= I18n.translate(locale, "Change password") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g"> <div class="pure-g">
@ -7,20 +7,20 @@
<div class="pure-u-1 pure-u-lg-3-5"> <div class="pure-u-1 pure-u-lg-3-5">
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/change_password?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/change_password?referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= translate(locale, "Change password") %></legend> <legend><%= I18n.translate(locale, "Change password") %></legend>
<fieldset> <fieldset>
<label for="password"><%= translate(locale, "Password") %> :</label> <label for="password"><%= I18n.translate(locale, "Password") %> :</label>
<input required class="pure-input-1" name="password" type="password" placeholder="<%= translate(locale, "Password") %>"> <input required class="pure-input-1" name="password" type="password" placeholder="<%= I18n.translate(locale, "Password") %>">
<label for="new_password[0]"><%= translate(locale, "New password") %> :</label> <label for="new_password[0]"><%= I18n.translate(locale, "New password") %> :</label>
<input required class="pure-input-1" name="new_password[0]" type="password" placeholder="<%= translate(locale, "New password") %>"> <input required class="pure-input-1" name="new_password[0]" type="password" placeholder="<%= I18n.translate(locale, "New password") %>">
<label for="new_password[1]"><%= translate(locale, "New password") %> :</label> <label for="new_password[1]"><%= I18n.translate(locale, "New password") %> :</label>
<input required class="pure-input-1" name="new_password[1]" type="password" placeholder="<%= translate(locale, "New password") %>"> <input required class="pure-input-1" name="new_password[1]" type="password" placeholder="<%= I18n.translate(locale, "New password") %>">
<button type="submit" name="action" value="change_password" class="pure-button pure-button-primary"> <button type="submit" name="action" value="change_password" class="pure-button pure-button-primary">
<%= translate(locale, "Change password") %> <%= I18n.translate(locale, "Change password") %>
</button> </button>
<input type="hidden" name="csrf_token" value="<%= HTML.escape(csrf_token) %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(csrf_token) %>">

View File

@ -1,20 +1,20 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Clear watch history") %> - Invidious</title> <title><%= I18n.translate(locale, "Clear watch history") %> - Invidious</title>
<% end %> <% end %>
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= translate(locale, "Clear watch history?") %></legend> <legend><%= I18n.translate(locale, "Clear watch history?") %></legend>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary"> <button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary">
<%= translate(locale, "Yes") %> <%= I18n.translate(locale, "Yes") %>
</button> </button>
</div> </div>
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<a class="pure-button" href="<%= URI.encode_www_form(referer) %>"> <a class="pure-button" href="<%= URI.encode_www_form(referer) %>">
<%= translate(locale, "No") %> <%= I18n.translate(locale, "No") %>
</a> </a>
</div> </div>
</div> </div>

View File

@ -1,67 +1,67 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Import and Export Data") %> - Invidious</title> <title><%= I18n.translate(locale, "Import and Export Data") %> - Invidious</title>
<% end %> <% end %>
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= URI.encode_www_form(referer) %>" method="post">
<fieldset> <fieldset>
<legend><%= translate(locale, "Import") %></legend> <legend><%= I18n.translate(locale, "Import") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_invidious"><%= translate(locale, "Import Invidious data") %></label> <label for="import_invidious"><%= I18n.translate(locale, "Import Invidious data") %></label>
<input type="file" id="import_invidious" name="import_invidious"> <input type="file" id="import_invidious" name="import_invidious">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_youtube"> <label for="import_youtube">
<a rel="noopener noreferrer" target="_blank" href="https://github.com/iv-org/documentation/blob/master/docs/export-youtube-subscriptions.md"> <a rel="noopener noreferrer" target="_blank" href="https://github.com/iv-org/documentation/blob/master/docs/export-youtube-subscriptions.md">
<%= translate(locale, "Import YouTube subscriptions") %> <%= I18n.translate(locale, "Import YouTube subscriptions") %>
</a> </a>
</label> </label>
<input type="file" id="import_youtube" name="import_youtube"> <input type="file" id="import_youtube" name="import_youtube">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_youtube_pl"><%= translate(locale, "Import YouTube playlist (.csv)") %></label> <label for="import_youtube_pl"><%= I18n.translate(locale, "Import YouTube playlist (.csv)") %></label>
<input type="file" id="import_youtube_pl" name="import_youtube_pl"> <input type="file" id="import_youtube_pl" name="import_youtube_pl">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_youtube_wh"><%= translate(locale, "Import YouTube watch history (.json)") %></label> <label for="import_youtube_wh"><%= I18n.translate(locale, "Import YouTube watch history (.json)") %></label>
<input type="file" id="import_youtube_wh" name="import_youtube_wh"> <input type="file" id="import_youtube_wh" name="import_youtube_wh">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_freetube"><%= translate(locale, "Import FreeTube subscriptions (.db)") %></label> <label for="import_freetube"><%= I18n.translate(locale, "Import FreeTube subscriptions (.db)") %></label>
<input type="file" id="import_freetube" name="import_freetube"> <input type="file" id="import_freetube" name="import_freetube">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_newpipe_subscriptions"><%= translate(locale, "Import NewPipe subscriptions (.json)") %></label> <label for="import_newpipe_subscriptions"><%= I18n.translate(locale, "Import NewPipe subscriptions (.json)") %></label>
<input type="file" id="import_newpipe_subscriptions" name="import_newpipe_subscriptions"> <input type="file" id="import_newpipe_subscriptions" name="import_newpipe_subscriptions">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_newpipe"><%= translate(locale, "Import NewPipe data (.zip)") %></label> <label for="import_newpipe"><%= I18n.translate(locale, "Import NewPipe data (.zip)") %></label>
<input type="file" id="import_newpipe" name="import_newpipe"> <input type="file" id="import_newpipe" name="import_newpipe">
</div> </div>
<div class="pure-controls"> <div class="pure-controls">
<button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Import") %></button> <button type="submit" class="pure-button pure-button-primary"><%= I18n.translate(locale, "Import") %></button>
</div> </div>
<legend><%= translate(locale, "Export") %></legend> <legend><%= I18n.translate(locale, "Export") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/subscription_manager?action_takeout=1"><%= translate(locale, "Export subscriptions as OPML") %></a> <a href="/subscription_manager?action_takeout=1"><%= I18n.translate(locale, "Export subscriptions as OPML") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/subscription_manager?action_takeout=1&format=newpipe"><%= translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %></a> <a href="/subscription_manager?action_takeout=1&format=newpipe"><%= I18n.translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/subscription_manager?action_takeout=1&format=json"><%= translate(locale, "Export data as JSON") %></a> <a href="/subscription_manager?action_takeout=1&format=json"><%= I18n.translate(locale, "Export data as JSON") %></a>
</div> </div>
</fieldset> </fieldset>
</form> </form>

View File

@ -1,20 +1,20 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Delete account") %> - Invidious</title> <title><%= I18n.translate(locale, "Delete account") %> - Invidious</title>
<% end %> <% end %>
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/delete_account?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/delete_account?referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= translate(locale, "Delete account?") %></legend> <legend><%= I18n.translate(locale, "Delete account?") %></legend>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<button type="submit" name="submit" value="delete_account" class="pure-button pure-button-primary"> <button type="submit" name="submit" value="delete_account" class="pure-button pure-button-primary">
<%= translate(locale, "Yes") %> <%= I18n.translate(locale, "Yes") %>
</button> </button>
</div> </div>
<div class="pure-u-1-2"> <div class="pure-u-1-2">
<a class="pure-button" href="<%= URI.encode_www_form(referer) %>"> <a class="pure-button" href="<%= URI.encode_www_form(referer) %>">
<%= translate(locale, "No") %> <%= I18n.translate(locale, "No") %>
</a> </a>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Log in") %> - Invidious</title> <title><%= I18n.translate(locale, "Log in") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g"> <div class="pure-g">
@ -13,15 +13,15 @@
<% if email %> <% if email %>
<input name="email" type="hidden" value="<%= HTML.escape(email) %>"> <input name="email" type="hidden" value="<%= HTML.escape(email) %>">
<% else %> <% else %>
<label for="email"><%= translate(locale, "User ID") %> :</label> <label for="email"><%= I18n.translate(locale, "User ID") %> :</label>
<input required class="pure-input-1" name="email" type="text" placeholder="<%= translate(locale, "User ID") %>"> <input required class="pure-input-1" name="email" type="text" placeholder="<%= I18n.translate(locale, "User ID") %>">
<% end %> <% end %>
<% if password %> <% if password %>
<input name="password" type="hidden" value="<%= HTML.escape(password) %>"> <input name="password" type="hidden" value="<%= HTML.escape(password) %>">
<% else %> <% else %>
<label for="password"><%= translate(locale, "Password") %> :</label> <label for="password"><%= I18n.translate(locale, "Password") %> :</label>
<input required class="pure-input-1" name="password" type="password" placeholder="<%= translate(locale, "Password") %>"> <input required class="pure-input-1" name="password" type="password" placeholder="<%= I18n.translate(locale, "Password") %>">
<% end %> <% end %>
<% if captcha %> <% if captcha %>
@ -30,15 +30,15 @@
<% captcha[:tokens].each_with_index do |token, i| %> <% captcha[:tokens].each_with_index do |token, i| %>
<input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>"> <input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>">
<% end %> <% end %>
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label> <label for="answer"><%= I18n.translate(locale, "Time (h:mm:ss):") %></label>
<input type="text" name="answer" type="text" placeholder="h:mm:ss"> <input type="text" name="answer" type="text" placeholder="h:mm:ss">
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary"> <button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
<%= translate(locale, "Register") %> <%= I18n.translate(locale, "Register") %>
</button> </button>
<% else %> <% else %>
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary"> <button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
<%= translate(locale, "Sign In") %>/<%= translate(locale, "Register") %> <%= I18n.translate(locale, "Sign In") %>/<%= I18n.translate(locale, "Register") %>
</button> </button>
<% end %> <% end %>
</fieldset> </fieldset>

View File

@ -1,49 +1,49 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Preferences") %> - Invidious</title> <title><%= I18n.translate(locale, "Preferences") %> - Invidious</title>
<% end %> <% end %>
<div class="h-box"> <div class="h-box">
<form class="pure-form pure-form-aligned" action="/preferences?referer=<%= URI.encode_www_form(referer) %>" method="post"> <form class="pure-form pure-form-aligned" action="/preferences?referer=<%= URI.encode_www_form(referer) %>" method="post">
<fieldset> <fieldset>
<legend><%= translate(locale, "preferences_category_player") %></legend> <legend><%= I18n.translate(locale, "preferences_category_player") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="video_loop"><%= translate(locale, "preferences_video_loop_label") %></label> <label for="video_loop"><%= I18n.translate(locale, "preferences_video_loop_label") %></label>
<input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>> <input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="preload"><%= translate(locale, "preferences_preload_label") %></label> <label for="preload"><%= I18n.translate(locale, "preferences_preload_label") %></label>
<input name="preload" id="preload" type="checkbox" <% if preferences.preload %>checked<% end %>> <input name="preload" id="preload" type="checkbox" <% if preferences.preload %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label> <label for="autoplay"><%= I18n.translate(locale, "preferences_autoplay_label") %></label>
<input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>> <input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="continue"><%= translate(locale, "preferences_continue_label") %></label> <label for="continue"><%= I18n.translate(locale, "preferences_continue_label") %></label>
<input name="continue" id="continue" type="checkbox" <% if preferences.continue %>checked<% end %>> <input name="continue" id="continue" type="checkbox" <% if preferences.continue %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="continue_autoplay"><%= translate(locale, "preferences_continue_autoplay_label") %></label> <label for="continue_autoplay"><%= I18n.translate(locale, "preferences_continue_autoplay_label") %></label>
<input name="continue_autoplay" id="continue_autoplay" type="checkbox" <% if preferences.continue_autoplay %>checked<% end %>> <input name="continue_autoplay" id="continue_autoplay" type="checkbox" <% if preferences.continue_autoplay %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="local"><%= translate(locale, "preferences_local_label") %></label> <label for="local"><%= I18n.translate(locale, "preferences_local_label") %></label>
<input name="local" id="local" type="checkbox" <% if preferences.local && !CONFIG.disabled?("local") %>checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>> <input name="local" id="local" type="checkbox" <% if preferences.local && !CONFIG.disabled?("local") %>checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="listen"><%= translate(locale, "preferences_listen_label") %></label> <label for="listen"><%= I18n.translate(locale, "preferences_listen_label") %></label>
<input name="listen" id="listen" type="checkbox" <% if preferences.listen %>checked<% end %>> <input name="listen" id="listen" type="checkbox" <% if preferences.listen %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="speed"><%= translate(locale, "preferences_speed_label") %></label> <label for="speed"><%= I18n.translate(locale, "preferences_speed_label") %></label>
<select name="speed" id="speed"> <select name="speed" id="speed">
<% {2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %> <% {2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %>
<option <% if preferences.speed == option %> selected <% end %>><%= option %></option> <option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
@ -52,11 +52,11 @@
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="quality"><%= translate(locale, "preferences_quality_label") %></label> <label for="quality"><%= I18n.translate(locale, "preferences_quality_label") %></label>
<select name="quality" id="quality"> <select name="quality" id="quality">
<% {"dash", "hd720", "medium", "small"}.each do |option| %> <% {"dash", "hd720", "medium", "small"}.each do |option| %>
<% if !(option == "dash" && CONFIG.disabled?("dash")) %> <% if !(option == "dash" && CONFIG.disabled?("dash")) %>
<option value="<%= option %>" <% if preferences.quality == option %> selected <% end %>><%= translate(locale, "preferences_quality_option_" + option) %></option> <option value="<%= option %>" <% if preferences.quality == option %> selected <% end %>><%= I18n.translate(locale, "preferences_quality_option_" + option) %></option>
<% end %> <% end %>
<% end %> <% end %>
</select> </select>
@ -64,74 +64,74 @@
<% if !CONFIG.disabled?("dash") %> <% if !CONFIG.disabled?("dash") %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="quality_dash"><%= translate(locale, "preferences_quality_dash_label") %></label> <label for="quality_dash"><%= I18n.translate(locale, "preferences_quality_dash_label") %></label>
<select name="quality_dash" id="quality_dash"> <select name="quality_dash" id="quality_dash">
<% {"auto", "best", "4320p", "2160p", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p", "worst"}.each do |option| %> <% {"auto", "best", "4320p", "2160p", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p", "worst"}.each do |option| %>
<option value="<%= option %>" <% if preferences.quality_dash == option %> selected <% end %>><%= translate(locale, "preferences_quality_dash_option_" + option) %></option> <option value="<%= option %>" <% if preferences.quality_dash == option %> selected <% end %>><%= I18n.translate(locale, "preferences_quality_dash_option_" + option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<% end %> <% end %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="volume"><%= translate(locale, "preferences_volume_label") %></label> <label for="volume"><%= I18n.translate(locale, "preferences_volume_label") %></label>
<input name="volume" id="volume" data-onrange="update_volume_value" type="range" min="0" max="100" step="5" value="<%= preferences.volume %>"> <input name="volume" id="volume" data-onrange="update_volume_value" type="range" min="0" max="100" step="5" value="<%= preferences.volume %>">
<span class="pure-form-message-inline" id="volume-value"><%= preferences.volume %></span> <span class="pure-form-message-inline" id="volume-value"><%= preferences.volume %></span>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="comments[0]"><%= translate(locale, "preferences_comments_label") %></label> <label for="comments[0]"><%= I18n.translate(locale, "preferences_comments_label") %></label>
<% preferences.comments.each_with_index do |comments, index| %> <% preferences.comments.each_with_index do |comments, index| %>
<select name="comments[<%= index %>]" id="comments[<%= index %>]"> <select name="comments[<%= index %>]" id="comments[<%= index %>]">
<% {"", "youtube", "reddit"}.each do |option| %> <% {"", "youtube", "reddit"}.each do |option| %>
<option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> <option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option>
<% end %> <% end %>
</select> </select>
<% end %> <% end %>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="captions[0]"><%= translate(locale, "preferences_captions_label") %></label> <label for="captions[0]"><%= I18n.translate(locale, "preferences_captions_label") %></label>
<% preferences.captions.each_with_index do |caption, index| %> <% preferences.captions.each_with_index do |caption, index| %>
<select class="pure-u-1-6" name="captions[<%= index %>]" id="captions[<%= index %>]"> <select class="pure-u-1-6" name="captions[<%= index %>]" id="captions[<%= index %>]">
<% Invidious::Videos::Captions::LANGUAGES.each do |option| %> <% Invidious::Videos::Captions::LANGUAGES.each do |option| %>
<option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> <option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option>
<% end %> <% end %>
</select> </select>
<% end %> <% end %>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="related_videos"><%= translate(locale, "preferences_related_videos_label") %></label> <label for="related_videos"><%= I18n.translate(locale, "preferences_related_videos_label") %></label>
<input name="related_videos" id="related_videos" type="checkbox" <% if preferences.related_videos %>checked<% end %>> <input name="related_videos" id="related_videos" type="checkbox" <% if preferences.related_videos %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="annotations"><%= translate(locale, "preferences_annotations_label") %></label> <label for="annotations"><%= I18n.translate(locale, "preferences_annotations_label") %></label>
<input name="annotations" id="annotations" type="checkbox" <% if preferences.annotations %>checked<% end %>> <input name="annotations" id="annotations" type="checkbox" <% if preferences.annotations %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="extend_desc"><%= translate(locale, "preferences_extend_desc_label") %></label> <label for="extend_desc"><%= I18n.translate(locale, "preferences_extend_desc_label") %></label>
<input name="extend_desc" id="extend_desc" type="checkbox" <% if preferences.extend_desc %>checked<% end %>> <input name="extend_desc" id="extend_desc" type="checkbox" <% if preferences.extend_desc %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="vr_mode"><%= translate(locale, "preferences_vr_mode_label") %></label> <label for="vr_mode"><%= I18n.translate(locale, "preferences_vr_mode_label") %></label>
<input name="vr_mode" id="vr_mode" type="checkbox" <% if preferences.vr_mode %>checked<% end %>> <input name="vr_mode" id="vr_mode" type="checkbox" <% if preferences.vr_mode %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="save_player_pos"><%= translate(locale, "preferences_save_player_pos_label") %></label> <label for="save_player_pos"><%= I18n.translate(locale, "preferences_save_player_pos_label") %></label>
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>> <input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
</div> </div>
<% if user = env.get?("user").try &.as(User) %> <% if user = env.get?("user").try &.as(User) %>
<% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %> <% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="default_playlist"><%= translate(locale, "preferences_default_playlist") %></label> <label for="default_playlist"><%= I18n.translate(locale, "preferences_default_playlist") %></label>
<select name="default_playlist" id="default_playlist"> <select name="default_playlist" id="default_playlist">
<option value=""><%= translate(locale, "preferences_default_playlist_none") %></option> <option value=""><%= I18n.translate(locale, "preferences_default_playlist_none") %></option>
<% playlists.each do |plid, playlist_title| %> <% playlists.each do |plid, playlist_title| %>
<option value="<%= plid %>" <%= "selected" if user.preferences.default_playlist == plid %>><%= HTML.escape(playlist_title) %></option> <option value="<%= plid %>" <%= "selected" if user.preferences.default_playlist == plid %>><%= HTML.escape(playlist_title) %></option>
<% end %> <% end %>
@ -139,46 +139,46 @@
</div> </div>
<% end %> <% end %>
<legend><%= translate(locale, "preferences_category_visual") %></legend> <legend><%= I18n.translate(locale, "preferences_category_visual") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="locale"><%= translate(locale, "preferences_locale_label") %></label> <label for="locale"><%= I18n.translate(locale, "preferences_locale_label") %></label>
<select name="locale" id="locale"> <select name="locale" id="locale">
<% LOCALES_LIST.each do |iso_name, full_name| %> <% I18n::LOCALES_LIST.each do |iso_name, full_name| %>
<option value="<%= iso_name %>" <% if preferences.locale == iso_name %> selected <% end %>><%= HTML.escape(full_name) %></option> <option value="<%= iso_name %>" <% if preferences.locale == iso_name %> selected <% end %>><%= HTML.escape(full_name) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="region"><%= translate(locale, "preferences_region_label") %></label> <label for="region"><%= I18n.translate(locale, "preferences_region_label") %></label>
<select name="region" id="region"> <select name="region" id="region">
<% CONTENT_REGIONS.each do |option| %> <% I18n::CONTENT_REGIONS.each do |option| %>
<option value="<%= option %>" <% if preferences.region == option %> selected <% end %>><%= option %></option> <option value="<%= option %>" <% if preferences.region == option %> selected <% end %>><%= option %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="player_style"><%= translate(locale, "preferences_player_style_label") %></label> <label for="player_style"><%= I18n.translate(locale, "preferences_player_style_label") %></label>
<select name="player_style" id="player_style"> <select name="player_style" id="player_style">
<% {"invidious", "youtube"}.each do |option| %> <% {"invidious", "youtube"}.each do |option| %>
<option value="<%= option %>" <% if preferences.player_style == option %> selected <% end %>><%= translate(locale, option) %></option> <option value="<%= option %>" <% if preferences.player_style == option %> selected <% end %>><%= I18n.translate(locale, option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="dark_mode"><%= translate(locale, "preferences_dark_mode_label") %></label> <label for="dark_mode"><%= I18n.translate(locale, "preferences_dark_mode_label") %></label>
<select name="dark_mode" id="dark_mode"> <select name="dark_mode" id="dark_mode">
<% {"", "light", "dark"}.each do |option| %> <% {"", "light", "dark"}.each do |option| %>
<option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= translate(locale, option.blank? ? "auto" : option) %></option> <option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "auto" : option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="thin_mode"><%= translate(locale, "preferences_thin_mode_label") %></label> <label for="thin_mode"><%= I18n.translate(locale, "preferences_thin_mode_label") %></label>
<input name="thin_mode" id="thin_mode" type="checkbox" <% if preferences.thin_mode %>checked<% end %>> <input name="thin_mode" id="thin_mode" type="checkbox" <% if preferences.thin_mode %>checked<% end %>>
</div> </div>
@ -198,125 +198,120 @@
%> %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="default_home"><%= translate(locale, "preferences_default_home_label") %></label> <label for="default_home"><%= I18n.translate(locale, "preferences_default_home_label") %></label>
<select name="default_home" id="default_home"> <select name="default_home" id="default_home">
<% feed_options.each do |option| %> <% feed_options.each do |option| %>
<option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option> <option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "Search" : option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="feed_menu"><%= translate(locale, "preferences_feed_menu_label") %></label> <label for="feed_menu"><%= I18n.translate(locale, "preferences_feed_menu_label") %></label>
<% (feed_options.size - 1).times do |index| %> <% (feed_options.size - 1).times do |index| %>
<select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]"> <select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
<% feed_options.each do |option| %> <% feed_options.each do |option| %>
<option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option> <option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "Search" : option) %></option>
<% end %> <% end %>
</select> </select>
<% end %> <% end %>
</div> </div>
<% if env.get? "user" %> <% if env.get? "user" %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="show_nick"><%= translate(locale, "preferences_show_nick_label") %></label> <label for="show_nick"><%= I18n.translate(locale, "preferences_show_nick_label") %></label>
<input name="show_nick" id="show_nick" type="checkbox" <% if preferences.show_nick %>checked<% end %>> <input name="show_nick" id="show_nick" type="checkbox" <% if preferences.show_nick %>checked<% end %>>
</div> </div>
<% end %> <% end %>
<legend><%= translate(locale, "preferences_category_misc") %></legend> <legend><%= I18n.translate(locale, "preferences_category_misc") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="automatic_instance_redirect"><%= translate(locale, "preferences_automatic_instance_redirect_label") %></label> <label for="automatic_instance_redirect"><%= I18n.translate(locale, "preferences_automatic_instance_redirect_label") %></label>
<input name="automatic_instance_redirect" id="automatic_instance_redirect" type="checkbox" <% if preferences.automatic_instance_redirect %>checked<% end %>> <input name="automatic_instance_redirect" id="automatic_instance_redirect" type="checkbox" <% if preferences.automatic_instance_redirect %>checked<% end %>>
</div> </div>
<% if env.get? "user" %> <% if env.get? "user" %>
<legend><%= translate(locale, "preferences_category_subscription") %></legend> <legend><%= I18n.translate(locale, "preferences_category_subscription") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="watch_history"><%= translate(locale, "preferences_watch_history_label") %></label> <label for="watch_history"><%= I18n.translate(locale, "preferences_watch_history_label") %></label>
<input name="watch_history" id="watch_history" type="checkbox" <% if preferences.watch_history %>checked<% end %>> <input name="watch_history" id="watch_history" type="checkbox" <% if preferences.watch_history %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="annotations_subscribed"><%= translate(locale, "preferences_annotations_subscribed_label") %></label> <label for="annotations_subscribed"><%= I18n.translate(locale, "preferences_annotations_subscribed_label") %></label>
<input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>> <input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="max_results"><%= translate(locale, "preferences_max_results_label") %></label> <label for="max_results"><%= I18n.translate(locale, "preferences_max_results_label") %></label>
<input name="max_results" id="max_results" type="number" value="<%= preferences.max_results %>"> <input name="max_results" id="max_results" type="number" value="<%= preferences.max_results %>">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="sort"><%= translate(locale, "preferences_sort_label") %></label> <label for="sort"><%= I18n.translate(locale, "preferences_sort_label") %></label>
<select name="sort" id="sort"> <select name="sort" id="sort">
<% {"published", "published - reverse", "alphabetically", "alphabetically - reverse", "channel name", "channel name - reverse"}.each do |option| %> <% {"published", "published - reverse", "alphabetically", "alphabetically - reverse", "channel name", "channel name - reverse"}.each do |option| %>
<option value="<%= option %>" <% if preferences.sort == option %> selected <% end %>><%= translate(locale, option) %></option> <option value="<%= option %>" <% if preferences.sort == option %> selected <% end %>><%= I18n.translate(locale, option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<% if preferences.unseen_only %> <% if preferences.unseen_only %>
<label for="latest_only"><%= translate(locale, "Only show latest unwatched video from channel: ") %></label> <label for="latest_only"><%= I18n.translate(locale, "Only show latest unwatched video from channel: ") %></label>
<% else %> <% else %>
<label for="latest_only"><%= translate(locale, "Only show latest video from channel: ") %></label> <label for="latest_only"><%= I18n.translate(locale, "Only show latest video from channel: ") %></label>
<% end %> <% end %>
<input name="latest_only" id="latest_only" type="checkbox" <% if preferences.latest_only %>checked<% end %>> <input name="latest_only" id="latest_only" type="checkbox" <% if preferences.latest_only %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="unseen_only"><%= translate(locale, "preferences_unseen_only_label") %></label> <label for="unseen_only"><%= I18n.translate(locale, "preferences_unseen_only_label") %></label>
<input name="unseen_only" id="unseen_only" type="checkbox" <% if preferences.unseen_only %>checked<% end %>> <input name="unseen_only" id="unseen_only" type="checkbox" <% if preferences.unseen_only %>checked<% end %>>
</div> </div>
<% if CONFIG.enable_user_notifications %> <% if CONFIG.enable_user_notifications %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="notifications_only"><%= translate(locale, "preferences_notifications_only_label") %></label> <label for="notifications_only"><%= I18n.translate(locale, "preferences_notifications_only_label") %></label>
<input name="notifications_only" id="notifications_only" type="checkbox" <% if preferences.notifications_only %>checked<% end %>> <input name="notifications_only" id="notifications_only" type="checkbox" <% if preferences.notifications_only %>checked<% end %>>
</div> </div>
<% # Web notifications are only supported over HTTPS %> <% # Web notifications are only supported over HTTPS %>
<% if Kemal.config.ssl || CONFIG.https_only %> <% if Kemal.config.ssl || CONFIG.https_only %>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="#" data-onclick="notification_requestPermission"><%= translate(locale, "Enable web notifications") %></a> <a href="#" data-onclick="notification_requestPermission"><%= I18n.translate(locale, "Enable web notifications") %></a>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>
<% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(Invidious::User).email %> <% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(Invidious::User).email %>
<legend><%= translate(locale, "preferences_category_admin") %></legend> <legend><%= I18n.translate(locale, "preferences_category_admin") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="admin_default_home"><%= translate(locale, "preferences_default_home_label") %></label> <label for="admin_default_home"><%= I18n.translate(locale, "preferences_default_home_label") %></label>
<select name="admin_default_home" id="admin_default_home"> <select name="admin_default_home" id="admin_default_home">
<% feed_options.each do |option| %> <% feed_options.each do |option| %>
<option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> <option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option>
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="admin_feed_menu"><%= translate(locale, "preferences_feed_menu_label") %></label> <label for="admin_feed_menu"><%= I18n.translate(locale, "preferences_feed_menu_label") %></label>
<% (feed_options.size - 1).times do |index| %> <% (feed_options.size - 1).times do |index| %>
<select name="admin_feed_menu[<%= index %>]" id="admin_feed_menu[<%= index %>]"> <select name="admin_feed_menu[<%= index %>]" id="admin_feed_menu[<%= index %>]">
<% feed_options.each do |option| %> <% feed_options.each do |option| %>
<option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option> <option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option>
<% end %> <% end %>
</select> </select>
<% end %> <% end %>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="popular_enabled"><%= translate(locale, "Popular enabled: ") %></label> <label for="popular_enabled"><%= I18n.translate(locale, "Popular enabled: ") %></label>
<input name="popular_enabled" id="popular_enabled" type="checkbox" <% if CONFIG.page_enabled?("popular") %>checked<% end %>> <input name="popular_enabled" id="popular_enabled" type="checkbox" <% if CONFIG.popular_enabled %>checked<% end %>>
</div>
<div class="pure-control-group">
<label for="trending_enabled"><%= translate(locale, "preferences_trending_enabled_label") %></label>
<input name="trending_enabled" id="trending_enabled" type="checkbox" <% if CONFIG.page_enabled?("trending") %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
@ -325,69 +320,69 @@
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="captcha_enabled"><%= translate(locale, "CAPTCHA enabled: ") %></label> <label for="captcha_enabled"><%= I18n.translate(locale, "CAPTCHA enabled: ") %></label>
<input name="captcha_enabled" id="captcha_enabled" type="checkbox" <% if CONFIG.captcha_enabled %>checked<% end %>> <input name="captcha_enabled" id="captcha_enabled" type="checkbox" <% if CONFIG.captcha_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="login_enabled"><%= translate(locale, "Login enabled: ") %></label> <label for="login_enabled"><%= I18n.translate(locale, "Login enabled: ") %></label>
<input name="login_enabled" id="login_enabled" type="checkbox" <% if CONFIG.login_enabled %>checked<% end %>> <input name="login_enabled" id="login_enabled" type="checkbox" <% if CONFIG.login_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="registration_enabled"><%= translate(locale, "Registration enabled: ") %></label> <label for="registration_enabled"><%= I18n.translate(locale, "Registration enabled: ") %></label>
<input name="registration_enabled" id="registration_enabled" type="checkbox" <% if CONFIG.registration_enabled %>checked<% end %>> <input name="registration_enabled" id="registration_enabled" type="checkbox" <% if CONFIG.registration_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="statistics_enabled"><%= translate(locale, "Report statistics: ") %></label> <label for="statistics_enabled"><%= I18n.translate(locale, "Report statistics: ") %></label>
<input name="statistics_enabled" id="statistics_enabled" type="checkbox" <% if CONFIG.statistics_enabled %>checked<% end %>> <input name="statistics_enabled" id="statistics_enabled" type="checkbox" <% if CONFIG.statistics_enabled %>checked<% end %>>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="modified_source_code_url"><%= translate(locale, "adminprefs_modified_source_code_url_label") %></label> <label for="modified_source_code_url"><%= I18n.translate(locale, "adminprefs_modified_source_code_url_label") %></label>
<input name="modified_source_code_url" id="modified_source_code_url" type="url" value="<%= CONFIG.modified_source_code_url %>"> <input name="modified_source_code_url" id="modified_source_code_url" type="url" value="<%= CONFIG.modified_source_code_url %>">
</div> </div>
<% end %> <% end %>
<% if env.get? "user" %> <% if env.get? "user" %>
<legend><%= translate(locale, "preferences_category_data") %></legend> <legend><%= I18n.translate(locale, "preferences_category_data") %></legend>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Clear watch history") %></a> <a href="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Clear watch history") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/change_password?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Change password") %></a> <a href="/change_password?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Change password") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Import/export data") %></a> <a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Import/export data") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a> <a href="/subscription_manager"><%= I18n.translate(locale, "Manage subscriptions") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/token_manager"><%= translate(locale, "Manage tokens") %></a> <a href="/token_manager"><%= I18n.translate(locale, "Manage tokens") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/feed/playlists"><%= translate(locale, "View all playlists") %></a> <a href="/feed/playlists"><%= I18n.translate(locale, "View all playlists") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/feed/history"><%= translate(locale, "Watch history") %></a> <a href="/feed/history"><%= I18n.translate(locale, "Watch history") %></a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="/delete_account?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Delete account") %></a> <a href="/delete_account?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Delete account") %></a>
</div> </div>
<% end %> <% end %>
<div class="pure-controls"> <div class="pure-controls">
<button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Save preferences") %></button> <button type="submit" class="pure-button pure-button-primary"><%= I18n.translate(locale, "Save preferences") %></button>
</div> </div>
</fieldset> </fieldset>
</form> </form>

View File

@ -1,26 +1,26 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Subscription manager") %> - Invidious</title> <title><%= I18n.translate(locale, "Subscription manager") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3> <h3>
<a href="/feed/subscriptions"> <a href="/feed/subscriptions">
<%= translate_count(locale, "generic_subscriptions_count", subscriptions.size, NumberFormatting::HtmlSpan) %> <%= I18n.translate_count(locale, "generic_subscriptions_count", subscriptions.size, I18n::NumberFormatting::HtmlSpan) %>
</a> </a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:center"> <h3 style="text-align:center">
<a href="/feed/history"> <a href="/feed/history">
<%= translate(locale, "Watch history") %> <%= I18n.translate(locale, "Watch history") %>
</a> </a>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3 style="text-align:right"> <h3 style="text-align:right">
<a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"> <a href="/data_control?referer=<%= URI.encode_www_form(referer) %>">
<%= translate(locale, "Import/export") %> <%= I18n.translate(locale, "Import/export") %>
</a> </a>
</h3> </h3>
</div> </div>
@ -39,7 +39,7 @@
<h3 style="padding-right:0.5em"> <h3 style="padding-right:0.5em">
<form data-onsubmit="return_false" action="/subscription_ajax?action=remove_subscriptions&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post"> <form data-onsubmit="return_false" action="/subscription_ajax?action=remove_subscriptions&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input style="all:unset" type="submit" data-onclick="remove_subscription" data-ucid="<%= channel.id %>" value="<%= translate(locale, "unsubscribe") %>"> <input style="all:unset" type="submit" data-onclick="remove_subscription" data-ucid="<%= channel.id %>" value="<%= I18n.translate(locale, "unsubscribe") %>">
</form> </form>
</h3> </h3>
</div> </div>

View File

@ -1,17 +1,17 @@
<% content_for "header" do %> <% content_for "header" do %>
<title><%= translate(locale, "Token manager") %> - Invidious</title> <title><%= I18n.translate(locale, "Token manager") %> - Invidious</title>
<% end %> <% end %>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<h3> <h3>
<%= translate_count(locale, "tokens_count", tokens.size, NumberFormatting::HtmlSpan) %> <%= I18n.translate_count(locale, "tokens_count", tokens.size, I18n::NumberFormatting::HtmlSpan) %>
</h3> </h3>
</div> </div>
<div class="pure-u-1-3"></div> <div class="pure-u-1-3"></div>
<div class="pure-u-1-3" style="text-align:right"> <div class="pure-u-1-3" style="text-align:right">
<h3> <h3>
<a href="/preferences?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Preferences") %></a> <a href="/preferences?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Preferences") %></a>
</h3> </h3>
</div> </div>
</div> </div>
@ -25,13 +25,13 @@
</h4> </h4>
</div> </div>
<div class="pure-u-1-5" style="text-align:center"> <div class="pure-u-1-5" style="text-align:center">
<h4><%= translate(locale, "`x` ago", recode_date(token[:issued], locale)) %></h4> <h4><%= I18n.translate(locale, "`x` ago", recode_date(token[:issued], locale)) %></h4>
</div> </div>
<div class="pure-u-1-5" style="text-align:right"> <div class="pure-u-1-5" style="text-align:right">
<h3 style="padding-right:0.5em"> <h3 style="padding-right:0.5em">
<form data-onsubmit="return_false" action="/token_ajax?action=revoke_token&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post"> <form data-onsubmit="return_false" action="/token_ajax?action=revoke_token&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input style="all:unset" type="submit" data-onclick="revoke_token" data-session="<%= token[:session] %>" value="<%= translate(locale, "revoke") %>"> <input style="all:unset" type="submit" data-onclick="revoke_token" data-session="<%= token[:session] %>" value="<%= I18n.translate(locale, "revoke") %>">
</form> </form>
</h3> </h3>
</div> </div>

View File

@ -35,11 +35,11 @@ we're going to need to do it here in order to allow for translations.
--> -->
<style> <style>
#descexpansionbutton ~ label > a::after { #descexpansionbutton ~ label > a::after {
content: "<%= translate(locale, "Show more") %>" content: "<%= I18n.translate(locale, "Show more") %>"
} }
#descexpansionbutton:checked ~ label > a::after { #descexpansionbutton:checked ~ label > a::after {
content: "<%= translate(locale, "Show less") %>" content: "<%= I18n.translate(locale, "Show less") %>"
} }
</style> </style>
<% end %> <% end %>
@ -53,12 +53,12 @@ we're going to need to do it here in order to allow for translations.
"length_seconds" => video.length_seconds.to_f, "length_seconds" => video.length_seconds.to_f,
"play_next" => !video.related_videos.empty? && !plid && params.continue, "play_next" => !video.related_videos.empty? && !plid && params.continue,
"next_video" => video.related_videos.select { |rv| rv["id"]? }[0]?.try &.["id"], "next_video" => video.related_videos.select { |rv| rv["id"]? }[0]?.try &.["id"],
"youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")), "youtube_comments_text" => HTML.escape(I18n.translate(locale, "View YouTube comments")),
"reddit_comments_text" => HTML.escape(translate(locale, "View Reddit comments")), "reddit_comments_text" => HTML.escape(I18n.translate(locale, "View Reddit comments")),
"reddit_permalink_text" => HTML.escape(translate(locale, "View more comments on Reddit")), "reddit_permalink_text" => HTML.escape(I18n.translate(locale, "View more comments on Reddit")),
"comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")), "comments_text" => HTML.escape(I18n.translate(locale, "View `x` comments", "{commentCount}")),
"hide_replies_text" => HTML.escape(translate(locale, "Hide replies")), "hide_replies_text" => HTML.escape(I18n.translate(locale, "Hide replies")),
"show_replies_text" => HTML.escape(translate(locale, "Show replies")), "show_replies_text" => HTML.escape(I18n.translate(locale, "Show replies")),
"params" => params, "params" => params,
"preferences" => preferences, "preferences" => preferences,
"premiere_timestamp" => video.premiere_timestamp.try &.to_unix, "premiere_timestamp" => video.premiere_timestamp.try &.to_unix,
@ -79,11 +79,11 @@ we're going to need to do it here in order to allow for translations.
<h1> <h1>
<%= title %> <%= title %>
<% if params.listen %> <% if params.listen %>
<a title="<%=translate(locale, "Video mode")%>" id="link-iv-listen" data-base-url="/watch?<%= env.params.query %>&listen=0" href="/watch?<%= env.params.query %>&listen=0"> <a title="<%=I18n.translate(locale, "Video mode")%>" id="link-iv-listen" data-base-url="/watch?<%= env.params.query %>&listen=0" href="/watch?<%= env.params.query %>&listen=0">
<i class="icon ion-ios-videocam"></i> <i class="icon ion-ios-videocam"></i>
</a> </a>
<% else %> <% else %>
<a title="<%=translate(locale, "Audio mode")%>" id="link-iv-listen" data-base-url="/watch?<%= env.params.query %>&listen=1" href="/watch?<%= env.params.query %>&listen=1"> <a title="<%=I18n.translate(locale, "Audio mode")%>" id="link-iv-listen" data-base-url="/watch?<%= env.params.query %>&listen=1" href="/watch?<%= env.params.query %>&listen=1">
<i class="icon ion-md-headset"></i> <i class="icon ion-md-headset"></i>
</a> </a>
<% end %> <% end %>
@ -91,7 +91,7 @@ we're going to need to do it here in order to allow for translations.
<% if !video.is_listed %> <% if !video.is_listed %>
<h3> <h3>
<i class="icon ion-ios-unlock"></i> <%= translate(locale, "Unlisted") %> <i class="icon ion-ios-unlock"></i> <%= I18n.translate(locale, "Unlisted") %>
</h3> </h3>
<% end %> <% end %>
@ -101,11 +101,11 @@ we're going to need to do it here in order to allow for translations.
</h3> </h3>
<% elsif video.premiere_timestamp.try &.> Time.utc %> <% elsif video.premiere_timestamp.try &.> Time.utc %>
<h3> <h3>
<%= video.premiere_timestamp.try { |t| translate(locale, "Premieres in `x`", recode_date((t - Time.utc).ago, locale)) } %> <%= video.premiere_timestamp.try { |t| I18n.translate(locale, "Premieres in `x`", recode_date((t - Time.utc).ago, locale)) } %>
</h3> </h3>
<% elsif video.live_now %> <% elsif video.live_now %>
<h3> <h3>
<%= video.premiere_timestamp.try { |t| translate(locale, "videoinfo_started_streaming_x_ago", recode_date((Time.utc - t).ago, locale)) } %> <%= video.premiere_timestamp.try { |t| I18n.translate(locale, "videoinfo_started_streaming_x_ago", recode_date((Time.utc - t).ago, locale)) } %>
</h3> </h3>
<% end %> <% end %>
</div> </div>
@ -124,13 +124,13 @@ we're going to need to do it here in order to allow for translations.
link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param) link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param)
end end
-%> -%>
<a id="link-yt-watch" rel="noreferrer noopener" data-base-url="<%= link_yt_watch %>" href="<%= link_yt_watch %>"><%= translate(locale, "videoinfo_watch_on_youTube") %></a> <a id="link-yt-watch" rel="noreferrer noopener" data-base-url="<%= link_yt_watch %>" href="<%= link_yt_watch %>"><%= I18n.translate(locale, "videoinfo_watch_on_youTube") %></a>
(<a id="link-yt-embed" rel="noopener" referrerpolicy="origin-when-cross-origin" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= translate(locale, "videoinfo_youTube_embed_link") %></a>) (<a id="link-yt-embed" rel="noopener" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= I18n.translate(locale, "videoinfo_youTube_embed_link") %></a>)
</span> </span>
<p id="watch-on-another-invidious-instance"> <p id="watch-on-another-invidious-instance">
<%- link_iv_other = IV::Frontend::Misc.redirect_url(env) -%> <%- link_iv_other = IV::Frontend::Misc.redirect_url(env) -%>
<a id="link-iv-other" data-base-url="<%= link_iv_other %>" href="<%= link_iv_other %>"><%= translate(locale, "Switch Invidious Instance") %></a> <a id="link-iv-other" data-base-url="<%= link_iv_other %>" href="<%= link_iv_other %>"><%= I18n.translate(locale, "Switch Invidious Instance") %></a>
</p> </p>
<p id="embed-link"> <p id="embed-link">
@ -141,17 +141,17 @@ we're going to need to do it here in order to allow for translations.
link_iv_embed = URI.new(path: "/embed/#{id}") link_iv_embed = URI.new(path: "/embed/#{id}")
link_iv_embed = IV::HttpServer::Utils.add_params_to_url(link_iv_embed, params_iv_embed) link_iv_embed = IV::HttpServer::Utils.add_params_to_url(link_iv_embed, params_iv_embed)
-%> -%>
<a id="link-iv-embed" data-base-url="<%= link_iv_embed %>" href="<%= link_iv_embed %>"><%= translate(locale, "videoinfo_invidious_embed_link") %></a> <a id="link-iv-embed" data-base-url="<%= link_iv_embed %>" href="<%= link_iv_embed %>"><%= I18n.translate(locale, "videoinfo_invidious_embed_link") %></a>
</p> </p>
<p id="annotations"> <p id="annotations">
<% if params.annotations %> <% if params.annotations %>
<a href="/watch?<%= env.params.query %>&iv_load_policy=3"> <a href="/watch?<%= env.params.query %>&iv_load_policy=3">
<%= translate(locale, "Hide annotations") %> <%= I18n.translate(locale, "Hide annotations") %>
</a> </a>
<% else %> <% else %>
<a href="/watch?<%= env.params.query %>&iv_load_policy=1"> <a href="/watch?<%= env.params.query %>&iv_load_policy=1">
<%=translate(locale, "Show annotations")%> <%=I18n.translate(locale, "Show annotations")%>
</a> </a>
<% end %> <% end %>
</p> </p>
@ -161,7 +161,7 @@ we're going to need to do it here in order to allow for translations.
<% if !playlists.empty? %> <% if !playlists.empty? %>
<form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax?action=add_video" method="post" target="_blank"> <form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax?action=add_video" method="post" target="_blank">
<div class="pure-control-group"> <div class="pure-control-group">
<label for="playlist_id"><%= translate(locale, "Add to playlist: ") %></label> <label for="playlist_id"><%= I18n.translate(locale, "Add to playlist: ") %></label>
<select style="width:100%" name="playlist_id" id="playlist_id"> <select style="width:100%" name="playlist_id" id="playlist_id">
<% playlists.each do |plid, playlist_title| %> <% playlists.each do |plid, playlist_title| %>
<option data-plid="<%= plid %>" value="<%= plid %>" <%= "selected" if user.preferences.default_playlist == plid %>><%= HTML.escape(playlist_title) %></option> <option data-plid="<%= plid %>" value="<%= plid %>" <%= "selected" if user.preferences.default_playlist == plid %>><%= HTML.escape(playlist_title) %></option>
@ -172,7 +172,7 @@ we're going to need to do it here in order to allow for translations.
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<input type="hidden" name="video_id" value="<%= video.id %>"> <input type="hidden" name="video_id" value="<%= video.id %>">
<button data-onclick="add_playlist_video" data-id="<%= video.id %>" type="submit" class="pure-button pure-button-primary"> <button data-onclick="add_playlist_video" data-id="<%= video.id %>" type="submit" class="pure-button pure-button-primary">
<b><%= translate(locale, "Add to playlist") %></b> <b><%= I18n.translate(locale, "Add to playlist") %></b>
</button> </button>
</form> </form>
<script id="playlist_data" type="application/json"> <script id="playlist_data" type="application/json">
@ -191,7 +191,7 @@ we're going to need to do it here in order to allow for translations.
<p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p> <p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
<p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p> <p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
<p id="dislikes" style="display: none; visibility: hidden;"></p> <p id="dislikes" style="display: none; visibility: hidden;"></p>
<p id="genre"><%= translate(locale, "Genre: ") %> <p id="genre"><%= I18n.translate(locale, "Genre: ") %>
<% if !video.genre_url %> <% if !video.genre_url %>
<%= video.genre %> <%= video.genre %>
<% else %> <% else %>
@ -200,21 +200,21 @@ we're going to need to do it here in order to allow for translations.
</p> </p>
<% if video.license %> <% if video.license %>
<% if video.license.empty? %> <% if video.license.empty? %>
<p id="license"><%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %></p> <p id="license"><%= I18n.translate(locale, "License: ") %><%= I18n.translate(locale, "Standard YouTube license") %></p>
<% else %> <% else %>
<p id="license"><%= translate(locale, "License: ") %><%= video.license %></p> <p id="license"><%= I18n.translate(locale, "License: ") %><%= video.license %></p>
<% end %> <% end %>
<% end %> <% end %>
<p id="family_friendly"><%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %></p> <p id="family_friendly"><%= I18n.translate(locale, "Family friendly? ") %><%= I18n.translate_bool(locale, video.is_family_friendly) %></p>
<p id="wilson" style="display: none; visibility: hidden;"></p> <p id="wilson" style="display: none; visibility: hidden;"></p>
<p id="rating" style="display: none; visibility: hidden;"></p> <p id="rating" style="display: none; visibility: hidden;"></p>
<p id="engagement" style="display: none; visibility: hidden;"></p> <p id="engagement" style="display: none; visibility: hidden;"></p>
<% if video.allowed_regions.size != REGIONS.size %> <% if video.allowed_regions.size != REGIONS.size %>
<p id="allowed_regions"> <p id="allowed_regions">
<% if video.allowed_regions.size < REGIONS.size // 2 %> <% if video.allowed_regions.size < REGIONS.size // 2 %>
<%= translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %> <%= I18n.translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %>
<% else %> <% else %>
<%= translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %> <%= I18n.translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %>
<% end %> <% end %>
</p> </p>
<% end %> <% end %>
@ -246,9 +246,9 @@ we're going to need to do it here in order to allow for translations.
<div class="h-box"> <div class="h-box">
<p id="published-date"> <p id="published-date">
<% if video.premiere_timestamp.try &.> Time.utc %> <% if video.premiere_timestamp.try &.> Time.utc %>
<b><%= video.premiere_timestamp.try { |t| translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %></b> <b><%= video.premiere_timestamp.try { |t| I18n.translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %></b>
<% else %> <% else %>
<b><%= translate(locale, "Shared `x`", video.published.to_s("%B %-d, %Y")) %></b> <b><%= I18n.translate(locale, "Shared `x`", video.published.to_s("%B %-d, %Y")) %></b>
<% end %> <% end %>
</p> </p>
@ -270,7 +270,7 @@ we're going to need to do it here in order to allow for translations.
<input id="music-desc-expansion" type="checkbox"/> <input id="music-desc-expansion" type="checkbox"/>
<label for="music-desc-expansion"> <label for="music-desc-expansion">
<h3 id="music-description-title"> <h3 id="music-description-title">
<%= translate(locale, "Music in this video") %> <%= I18n.translate(locale, "Music in this video") %>
<span class="icon ion-ios-arrow-up"></span> <span class="icon ion-ios-arrow-up"></span>
<span class="icon ion-ios-arrow-down"></span> <span class="icon ion-ios-arrow-down"></span>
</h3> </h3>
@ -279,9 +279,9 @@ we're going to need to do it here in order to allow for translations.
<div id="music-description-box"> <div id="music-description-box">
<% video.music.each do |music| %> <% video.music.each do |music| %>
<div class="music-item"> <div class="music-item">
<p class="music-song"><%= translate(locale, "Song: ") %><%= music.song %></p> <p class="music-song"><%= I18n.translate(locale, "Song: ") %><%= music.song %></p>
<p class="music-artist"><%= translate(locale, "Artist: ") %><%= music.artist %></p> <p class="music-artist"><%= I18n.translate(locale, "Artist: ") %><%= music.artist %></p>
<p class="music-album"><%= translate(locale, "Album: ") %><%= music.album %></p> <p class="music-album"><%= I18n.translate(locale, "Album: ") %><%= music.album %></p>
</div> </div>
<% end %> <% end %>
</div> </div>
@ -294,7 +294,7 @@ we're going to need to do it here in order to allow for translations.
<% else %> <% else %>
<noscript> <noscript>
<a href="/watch?<%= env.params.query %>&nojs=1"> <a href="/watch?<%= env.params.query %>&nojs=1">
<%= translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %> <%= I18n.translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %>
</a> </a>
</noscript> </noscript>
<% end %> <% end %>
@ -313,7 +313,7 @@ we're going to need to do it here in order to allow for translations.
<% if !video.related_videos.empty? %> <% if !video.related_videos.empty? %>
<div <% if plid %>style="display:none"<% end %>> <div <% if plid %>style="display:none"<% end %>>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="continue"><%= translate(locale, "preferences_continue_label") %></label> <label for="continue"><%= I18n.translate(locale, "preferences_continue_label") %></label>
<input name="continue" id="continue" type="checkbox" <% if params.continue %>checked<% end %>> <input name="continue" id="continue" type="checkbox" <% if params.continue %>checked<% end %>>
</div> </div>
<hr> <hr>
@ -356,7 +356,7 @@ we're going to need to do it here in order to allow for translations.
<div class="pure-u-10-24" style="text-align:right"> <div class="pure-u-10-24" style="text-align:right">
<b class="width:100%"><%= <b class="width:100%"><%=
views = short_text_to_number(rv["short_view_count"]? || "0") views = short_text_to_number(rv["short_view_count"]? || "0")
translate_count(locale, "generic_views_count", views, NumberFormatting::Short) I18n.translate_count(locale, "generic_views_count", views, I18n::NumberFormatting::Short)
%></b> %></b>
</div> </div>
</h5> </h5>