From a160c645c9c6299553a7cc0c89e5182f5d1288bb Mon Sep 17 00:00:00 2001
From: Omar Roth
Date: Thu, 20 Dec 2018 15:32:09 -0600
Subject: [PATCH] Add support for translations
---
src/invidious.cr | 219 ++++++++++++++-----
src/invidious/channels.cr | 14 +-
src/invidious/comments.cr | 24 +-
src/invidious/helpers/i18n.cr | 23 ++
src/invidious/mixes.cr | 8 +-
src/invidious/playlists.cr | 26 +--
src/invidious/trending.cr | 4 +-
src/invidious/users.cr | 55 +++--
src/invidious/views/channel.ecr | 24 +-
src/invidious/views/clear_watch_history.ecr | 14 +-
src/invidious/views/components/item.ecr | 12 +-
src/invidious/views/components/player.ecr | 4 +-
src/invidious/views/data_control.ecr | 29 +--
src/invidious/views/delete_account.ecr | 14 +-
src/invidious/views/history.ecr | 14 +-
src/invidious/views/index.ecr | 2 +-
src/invidious/views/licenses.ecr | 22 +-
src/invidious/views/login.ecr | 36 +--
src/invidious/views/playlist.ecr | 8 +-
src/invidious/views/popular.ecr | 4 +
src/invidious/views/preferences.ecr | 83 +++----
src/invidious/views/search.ecr | 8 +-
src/invidious/views/subscription_manager.ecr | 10 +-
src/invidious/views/subscriptions.ecr | 16 +-
src/invidious/views/template.ecr | 38 +++-
src/invidious/views/top.ecr | 4 +
src/invidious/views/trending.ecr | 2 +-
src/invidious/views/watch.ecr | 57 ++---
28 files changed, 502 insertions(+), 272 deletions(-)
create mode 100644 src/invidious/helpers/i18n.cr
diff --git a/src/invidious.cr b/src/invidious.cr
index e8d07fd4..e56103ae 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -88,6 +88,15 @@ REDDIT_URL = URI.parse("https://www.reddit.com")
LOGIN_URL = URI.parse("https://accounts.google.com")
TEXTCAPTCHA_URL = URI.parse("http://textcaptcha.com/omarroth@hotmail.com.json")
+LOCALES = {
+ "ar" => load_locale("ar"),
+ "de" => load_locale("de"),
+ "en-US" => load_locale("en-US"),
+ "nl" => load_locale("nl"),
+ "pl" => load_locale("pl"),
+ "ru" => load_locale("ru"),
+}
+
crawl_threads.times do
spawn do
crawl_videos(PG_DB)
@@ -147,6 +156,7 @@ before_all do |env|
env.set "challenge", challenge
env.set "token", token
+ locale = user.preferences.locale
env.set "user", user
env.set "sid", sid
end
@@ -158,6 +168,7 @@ before_all do |env|
env.set "challenge", challenge
env.set "token", token
+ locale = user.preferences.locale
env.set "user", user
env.set "sid", sid
rescue ex
@@ -165,6 +176,10 @@ before_all do |env|
end
end
+ locale = env.params.query["hl"]? || locale
+ locale ||= "en-US"
+ env.set "locale", locale
+
current_page = env.request.path
if env.request.query
query = HTTP::Params.parse(env.request.query.not_nil!)
@@ -180,7 +195,9 @@ before_all do |env|
end
get "/" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
user = env.get? "user"
+
if user
user = user.as(User)
if user.preferences.redirect_feed
@@ -192,12 +209,14 @@ get "/" do |env|
end
get "/licenses" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
rendered "licenses"
end
# Videos
get "/:id" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
id = env.params.url["id"]
if md = id.match(/[a-zA-Z0-9_-]{11}/)
@@ -219,6 +238,8 @@ get "/:id" do |env|
end
get "/watch" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
url = "/watch?" + env.params.query.to_s.gsub("%20", "").delete("+")
next env.redirect url
@@ -287,11 +308,11 @@ get "/watch" do |env|
if source == "youtube"
begin
- comment_html = JSON.parse(fetch_youtube_comments(id, "", proxies, "html"))["contentHtml"]
+ comment_html = JSON.parse(fetch_youtube_comments(id, "", proxies, "html", locale))["contentHtml"]
rescue ex
if preferences.comments[1] == "reddit"
comments, reddit_thread = fetch_reddit_comments(id)
- comment_html = template_reddit_comments(comments)
+ comment_html = template_reddit_comments(comments, locale)
comment_html = fill_links(comment_html, "https", "www.reddit.com")
comment_html = replace_links(comment_html)
@@ -300,18 +321,18 @@ get "/watch" do |env|
elsif source == "reddit"
begin
comments, reddit_thread = fetch_reddit_comments(id)
- comment_html = template_reddit_comments(comments)
+ comment_html = template_reddit_comments(comments, locale)
comment_html = fill_links(comment_html, "https", "www.reddit.com")
comment_html = replace_links(comment_html)
rescue ex
if preferences.comments[1] == "youtube"
- comment_html = JSON.parse(fetch_youtube_comments(id, "", proxies, "html"))["contentHtml"]
+ comment_html = JSON.parse(fetch_youtube_comments(id, "", proxies, "html", locale))["contentHtml"]
end
end
end
else
- comment_html = JSON.parse(fetch_youtube_comments(id, "", proxies, "html"))["contentHtml"]
+ comment_html = JSON.parse(fetch_youtube_comments(id, "", proxies, "html", locale))["contentHtml"]
end
comment_html ||= ""
@@ -383,6 +404,7 @@ get "/watch" do |env|
end
get "/embed/:id" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
id = env.params.url["id"]
if id.includes?("%20") || id.includes?("+") || env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
@@ -470,6 +492,8 @@ end
# Playlists
get "/playlist" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
plid = env.params.query["list"]?
if !plid
next env.redirect "/"
@@ -483,14 +507,14 @@ get "/playlist" do |env|
end
begin
- playlist = fetch_playlist(plid)
+ playlist = fetch_playlist(plid, locale)
rescue ex
error_message = ex.message
next templated "error"
end
begin
- videos = fetch_playlist_videos(plid, page, playlist.video_count)
+ videos = fetch_playlist_videos(plid, page, playlist.video_count, locale)
rescue ex
videos = [] of PlaylistVideo
end
@@ -499,6 +523,8 @@ get "/playlist" do |env|
end
get "/mix" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
rdid = env.params.query["list"]?
if !rdid
next env.redirect "/"
@@ -508,7 +534,7 @@ get "/mix" do |env|
continuation ||= rdid.lchop("RD")
begin
- mix = fetch_mix(rdid, continuation)
+ mix = fetch_mix(rdid, continuation, locale: locale)
rescue ex
error_message = ex.message
next templated "error"
@@ -520,6 +546,7 @@ end
# Search
get "/opensearch.xml" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
env.response.content_type = "application/opensearchdescription+xml"
XML.build(indent: " ", encoding: "UTF-8") do |xml|
@@ -535,6 +562,8 @@ get "/opensearch.xml" do |env|
end
get "/results" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
query = env.params.query["search_query"]?
query ||= env.params.query["q"]?
query ||= ""
@@ -550,6 +579,8 @@ get "/results" do |env|
end
get "/search" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
query = env.params.query["search_query"]?
query ||= env.params.query["q"]?
query ||= ""
@@ -629,6 +660,8 @@ end
# Users
get "/login" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
if user
next env.redirect "/feed/subscriptions"
@@ -668,6 +701,8 @@ end
# See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L79
post "/login" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
referer = get_referer(env, "/feed/subscriptions")
email = env.params.body["email"]?
@@ -754,7 +789,7 @@ post "/login" do |env|
headers["Cookie"] = URI.unescape(headers["Cookie"])
if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED"
- error_message = "Incorrect password"
+ error_message = translate(locale, "Incorrect password")
next templated "error"
end
@@ -775,7 +810,7 @@ post "/login" do |env|
if tfa[2] == "TWO_STEP_VERIFICATION"
if tfa[5] == "QUOTA_EXCEEDED"
- error_message = "Quota exceeded, try again in a few hours"
+ error_message = translate(locale, "Quota exceeded, try again in a few hours")
next templated "error"
end
@@ -806,7 +841,7 @@ post "/login" do |env|
challenge_results = JSON.parse(challenge_results)
if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED"
- error_message = "Invalid TFA code"
+ error_message = translate(locale, "Invalid TFA code")
next templated "error"
end
end
@@ -845,7 +880,7 @@ post "/login" do |env|
env.redirect referer
rescue ex
- error_message = "Login failed. This may be because two-factor authentication is not enabled on your account."
+ error_message = translate(locale, "Login failed. This may be because two-factor authentication is not enabled on your account.")
next templated "error"
end
elsif account_type == "invidious"
@@ -860,10 +895,10 @@ post "/login" do |env|
token = env.params.body["token"]?
begin
- validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB)
+ validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB, locale)
rescue ex
- if ex.message == "Invalid user"
- error_message = "Invalid answer"
+ if ex.message == translate(locale, "Invalid user")
+ error_message = translate(locale, "Invalid answer")
else
error_message = ex.message
end
@@ -878,16 +913,16 @@ post "/login" do |env|
found_valid_captcha = false
- error_message = "Invalid CAPTCHA"
+ error_message = translate(locale, "Invalid CAPTCHA")
challenges.each_with_index do |challenge, i|
begin
challenge = challenge[1]
token = tokens[i][1]
- validate_response(challenge, token, text_answer, "sign_in", HMAC_KEY, PG_DB)
+ validate_response(challenge, token, text_answer, "sign_in", HMAC_KEY, PG_DB, locale)
found_valid_captcha = true
rescue ex
- if ex.message == "Invalid user"
- error_message = "Invalid answer"
+ if ex.message == translate(locale, "Invalid user")
+ error_message = translate(locale, "Invalid answer")
else
error_message = ex.message
end
@@ -898,7 +933,7 @@ post "/login" do |env|
next templated "error"
end
else
- error_message = "CAPTCHA is a required field"
+ error_message = translate(locale, "CAPTCHA is a required field")
next templated "error"
end
@@ -906,12 +941,12 @@ post "/login" do |env|
action ||= "signin"
if !email
- error_message = "User ID is a required field"
+ error_message = translate(locale, "User ID is a required field")
next templated "error"
end
if !password
- error_message = "Password is a required field"
+ error_message = translate(locale, "Password is a required field")
next templated "error"
end
@@ -919,12 +954,12 @@ post "/login" do |env|
user = PG_DB.query_one?("SELECT * FROM users WHERE LOWER(email) = LOWER($1) AND password IS NOT NULL", email, as: User)
if !user
- error_message = "Invalid username or password"
+ error_message = translate(locale, "Invalid username or password")
next templated "error"
end
if !user.password
- error_message = "Please sign in using 'Sign in with Google'"
+ error_message = translate(locale, "Please sign in using 'Sign in with Google'")
next templated "error"
end
@@ -946,24 +981,24 @@ post "/login" do |env|
secure: secure, http_only: true)
end
else
- error_message = "Invalid username or password"
+ error_message = translate(locale, "Invalid username or password")
next templated "error"
end
elsif action == "register"
if password.empty?
- error_message = "Password cannot be empty"
+ error_message = translate(locale, "Password cannot be empty")
next templated "error"
end
# See https://security.stackexchange.com/a/39851
if password.size > 55
- error_message = "Password cannot be longer than 55 characters"
+ error_message = translate(locale, "Password cannot be longer than 55 characters")
next templated "error"
end
user = PG_DB.query_one?("SELECT * FROM users WHERE LOWER(email) = LOWER($1) AND password IS NOT NULL", email, as: User)
if user
- error_message = "Please sign in"
+ error_message = translate(locale, "Please sign in")
next templated "error"
end
@@ -1002,6 +1037,8 @@ post "/login" do |env|
end
get "/signout" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1012,7 +1049,7 @@ get "/signout" do |env|
token = env.params.query["token"]?
begin
- validate_response(challenge, token, user.email, "sign_out", HMAC_KEY, PG_DB)
+ validate_response(challenge, token, user.email, "sign_out", HMAC_KEY, PG_DB, locale)
rescue ex
error_message = ex.message
next templated "error"
@@ -1033,6 +1070,8 @@ get "/signout" do |env|
end
get "/preferences" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1045,6 +1084,8 @@ get "/preferences" do |env|
end
post "/preferences" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1093,6 +1134,9 @@ post "/preferences" do |env|
redirect_feed ||= "off"
redirect_feed = redirect_feed == "on"
+ locale = env.params.body["locale"]?.try &.as(String)
+ locale ||= "en-US"
+
dark_mode = env.params.body["dark_mode"]?.try &.as(String)
dark_mode ||= "off"
dark_mode = dark_mode == "on"
@@ -1131,6 +1175,7 @@ post "/preferences" do |env|
"captions" => captions,
"related_videos" => related_videos,
"redirect_feed" => redirect_feed,
+ "locale" => locale,
"dark_mode" => dark_mode,
"thin_mode" => thin_mode,
"max_results" => max_results,
@@ -1147,6 +1192,8 @@ post "/preferences" do |env|
end
get "/toggle_theme" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1167,6 +1214,8 @@ get "/toggle_theme" do |env|
end
get "/mark_watched" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env, "/feed/subscriptions")
@@ -1195,6 +1244,8 @@ get "/mark_watched" do |env|
end
get "/mark_unwatched" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env, "/feed/history")
@@ -1225,6 +1276,8 @@ end
# /modify_notifications?receive_all_updates=false&receive_no_updates=false
# will "unding" all subscriptions.
get "/modify_notifications" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1270,6 +1323,8 @@ get "/modify_notifications" do |env|
end
get "/subscription_manager" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env, "/")
@@ -1351,6 +1406,8 @@ get "/subscription_manager" do |env|
end
get "/data_control" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1364,6 +1421,8 @@ get "/data_control" do |env|
end
post "/data_control" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1495,6 +1554,8 @@ post "/data_control" do |env|
end
get "/subscription_ajax" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1574,6 +1635,8 @@ get "/subscription_ajax" do |env|
end
get "/delete_account" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1589,6 +1652,8 @@ get "/delete_account" do |env|
end
post "/delete_account" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1599,7 +1664,7 @@ post "/delete_account" do |env|
token = env.params.body["token"]?
begin
- validate_response(challenge, token, user.email, "delete_account", HMAC_KEY, PG_DB)
+ validate_response(challenge, token, user.email, "delete_account", HMAC_KEY, PG_DB, locale)
rescue ex
error_message = ex.message
next templated "error"
@@ -1619,6 +1684,8 @@ post "/delete_account" do |env|
end
get "/clear_watch_history" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1634,6 +1701,8 @@ get "/clear_watch_history" do |env|
end
post "/clear_watch_history" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1644,7 +1713,7 @@ post "/clear_watch_history" do |env|
token = env.params.body["token"]?
begin
- validate_response(challenge, token, user.email, "clear_watch_history", HMAC_KEY, PG_DB)
+ validate_response(challenge, token, user.email, "clear_watch_history", HMAC_KEY, PG_DB, locale)
rescue ex
error_message = ex.message
next templated "error"
@@ -1659,19 +1728,25 @@ end
# Feeds
get "/feed/top" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
templated "top"
end
get "/feed/popular" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
templated "popular"
end
get "/feed/trending" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
trending_type = env.params.query["type"]?
region = env.params.query["region"]?
begin
- trending = fetch_trending(trending_type, proxies, region)
+ trending = fetch_trending(trending_type, proxies, region, locale)
rescue ex
error_message = "#{ex.message}"
next templated "error"
@@ -1681,6 +1756,8 @@ get "/feed/trending" do |env|
end
get "/feed/subscriptions" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1814,6 +1891,8 @@ get "/feed/subscriptions" do |env|
end
get "/feed/history" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
referer = get_referer(env)
@@ -1837,11 +1916,13 @@ get "/feed/history" do |env|
end
get "/feed/channel/:ucid" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
env.response.content_type = "text/xml"
ucid = env.params.url["ucid"]
begin
- author, ucid, auto_generated = get_about_info(ucid)
+ author, ucid, auto_generated = get_about_info(ucid, locale)
rescue ex
error_message = ex.message
halt env, status_code: 500, response: error_message
@@ -1906,6 +1987,8 @@ get "/feed/channel/:ucid" do |env|
end
get "/feed/private" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
token = env.params.query["token"]?
if !token
@@ -1978,7 +2061,7 @@ get "/feed/private" do |env|
"xml:lang": "en-US") do
xml.element("link", "type": "text/html", rel: "alternate", href: "#{host_url}/feed/subscriptions")
xml.element("link", "type": "application/atom+xml", rel: "self", href: "#{host_url}#{path}?#{query}")
- xml.element("title") { xml.text "Invidious Private Feed for #{user.email}" }
+ xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) }
videos.each do |video|
xml.element("entry") do
@@ -2011,6 +2094,8 @@ get "/feed/private" do |env|
end
get "/feed/playlist/:plid" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
plid = env.params.url["plid"]
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"]?)
@@ -2047,6 +2132,8 @@ end
# YouTube appears to let users set a "brand" URL that
# is different from their username, so we convert that here
get "/c/:user" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
client = make_client(YT_URL)
user = env.params.url["user"]
@@ -2072,6 +2159,8 @@ get "/user/:user/videos" do |env|
end
get "/channel/:ucid" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
user = env.get? "user"
if user
user = user.as(User)
@@ -2088,7 +2177,7 @@ get "/channel/:ucid" do |env|
sort_by ||= "newest"
begin
- author, ucid, auto_generated, sub_count = get_about_info(ucid)
+ author, ucid, auto_generated, sub_count = get_about_info(ucid, locale)
rescue ex
error_message = ex.message
next templated "error"
@@ -2108,6 +2197,8 @@ get "/channel/:ucid" do |env|
end
get "/channel/:ucid/videos" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
ucid = env.params.url["ucid"]
params = env.request.query
@@ -2123,6 +2214,8 @@ end
# API Endpoints
get "/api/v1/captions/:id" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
env.response.content_type = "application/json"
id = env.params.url["id"]
@@ -2222,6 +2315,8 @@ get "/api/v1/captions/:id" do |env|
end
get "/api/v1/comments/:id" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
env.response.content_type = "application/json"
id = env.params.url["id"]
@@ -2237,7 +2332,7 @@ get "/api/v1/comments/:id" do |env|
if source == "youtube"
begin
- comments = fetch_youtube_comments(id, continuation, proxies, format)
+ comments = fetch_youtube_comments(id, continuation, proxies, format, locale)
rescue ex
error_message = {"error" => ex.message}.to_json
halt env, status_code: 500, response: error_message
@@ -2247,7 +2342,7 @@ get "/api/v1/comments/:id" do |env|
elsif source == "reddit"
begin
comments, reddit_thread = fetch_reddit_comments(id)
- content_html = template_reddit_comments(comments)
+ content_html = template_reddit_comments(comments, locale)
content_html = fill_links(content_html, "https", "www.reddit.com")
content_html = replace_links(content_html)
@@ -2276,6 +2371,8 @@ get "/api/v1/comments/:id" do |env|
end
get "/api/v1/insights/:id" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
id = env.params.url["id"]
env.response.content_type = "application/json"
@@ -2356,6 +2453,8 @@ get "/api/v1/insights/:id" do |env|
end
get "/api/v1/videos/:id" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
env.response.content_type = "application/json"
id = env.params.url["id"]
@@ -2388,7 +2487,7 @@ get "/api/v1/videos/:id" do |env|
json.field "description", description
json.field "descriptionHtml", video.description
json.field "published", video.published.to_unix
- json.field "publishedText", "#{recode_date(video.published)} ago"
+ json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published))
json.field "keywords", video.keywords
json.field "viewCount", video.views
@@ -2559,11 +2658,13 @@ get "/api/v1/videos/:id" do |env|
end
get "/api/v1/trending" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
region = env.params.query["region"]?
trending_type = env.params.query["type"]?
begin
- trending = fetch_trending(trending_type, proxies, region)
+ trending = fetch_trending(trending_type, proxies, region, locale)
rescue ex
error_message = {"error" => ex.message}.to_json
halt env, status_code: 500, response: error_message
@@ -2587,7 +2688,7 @@ get "/api/v1/trending" do |env|
json.field "authorUrl", "/channel/#{video.ucid}"
json.field "published", video.published.to_unix
- json.field "publishedText", "#{recode_date(video.published)} ago"
+ json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published))
json.field "description", video.description
json.field "descriptionHtml", video.description_html
json.field "liveNow", video.live_now
@@ -2603,6 +2704,8 @@ get "/api/v1/trending" do |env|
end
get "/api/v1/popular" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
videos = JSON.build do |json|
json.array do
popular_videos.each do |video|
@@ -2619,7 +2722,7 @@ get "/api/v1/popular" do |env|
json.field "authorId", video.ucid
json.field "authorUrl", "/channel/#{video.ucid}"
json.field "published", video.published.to_unix
- json.field "publishedText", "#{recode_date(video.published)} ago"
+ json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published))
end
end
end
@@ -2630,6 +2733,8 @@ get "/api/v1/popular" do |env|
end
get "/api/v1/top" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
videos = JSON.build do |json|
json.array do
top_videos.each do |video|
@@ -2647,7 +2752,7 @@ get "/api/v1/top" do |env|
json.field "authorId", video.ucid
json.field "authorUrl", "/channel/#{video.ucid}"
json.field "published", video.published.to_unix
- json.field "publishedText", "#{recode_date(video.published)} ago"
+ json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published))
description = video.description.gsub("
", "\n")
description = description.gsub("
", "\n")
@@ -2664,6 +2769,8 @@ get "/api/v1/top" do |env|
end
get "/api/v1/channels/:ucid" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
env.response.content_type = "application/json"
ucid = env.params.url["ucid"]
@@ -2671,7 +2778,7 @@ get "/api/v1/channels/:ucid" do |env|
sort_by ||= "newest"
begin
- author, ucid, auto_generated = get_about_info(ucid)
+ author, ucid, auto_generated = get_about_info(ucid, locale)
rescue ex
error_message = {"error" => ex.message}.to_json
halt env, status_code: 500, response: error_message
@@ -2817,7 +2924,7 @@ get "/api/v1/channels/:ucid" do |env|
json.field "viewCount", video.views
json.field "published", video.published.to_unix
- json.field "publishedText", "#{recode_date(video.published)} ago"
+ json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published))
json.field "lengthSeconds", video.length_seconds
json.field "liveNow", video.live_now
json.field "paid", video.paid
@@ -2860,6 +2967,8 @@ end
["/api/v1/channels/:ucid/videos", "/api/v1/channels/videos/:ucid"].each do |route|
get route do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
env.response.content_type = "application/json"
ucid = env.params.url["ucid"]
@@ -2869,7 +2978,7 @@ end
sort_by ||= "newest"
begin
- author, ucid, auto_generated = get_about_info(ucid)
+ author, ucid, auto_generated = get_about_info(ucid, locale)
rescue ex
error_message = {"error" => ex.message}.to_json
halt env, status_code: 500, response: error_message
@@ -2908,7 +3017,7 @@ end
json.field "viewCount", video.views
json.field "published", video.published.to_unix
- json.field "publishedText", "#{recode_date(video.published)} ago"
+ json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published))
json.field "lengthSeconds", video.length_seconds
json.field "liveNow", video.live_now
json.field "paid", video.paid
@@ -2923,6 +3032,8 @@ end
end
get "/api/v1/channels/search/:ucid" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
env.response.content_type = "application/json"
ucid = env.params.url["ucid"]
@@ -2957,7 +3068,7 @@ get "/api/v1/channels/search/:ucid" do |env|
json.field "viewCount", item.views
json.field "published", item.published.to_unix
- json.field "publishedText", "#{recode_date(item.published)} ago"
+ json.field "publishedText", translate(locale, "`x` ago", recode_date(item.published))
json.field "lengthSeconds", item.length_seconds
json.field "liveNow", item.live_now
json.field "paid", item.paid
@@ -3021,6 +3132,8 @@ get "/api/v1/channels/search/:ucid" do |env|
end
get "/api/v1/search" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
env.response.content_type = "application/json"
query = env.params.query["q"]?
@@ -3080,7 +3193,7 @@ get "/api/v1/search" do |env|
json.field "viewCount", item.views
json.field "published", item.published.to_unix
- json.field "publishedText", "#{recode_date(item.published)} ago"
+ json.field "publishedText", translate(locale, "`x` ago", recode_date(item.published))
json.field "lengthSeconds", item.length_seconds
json.field "liveNow", item.live_now
json.field "paid", item.paid
@@ -3144,6 +3257,8 @@ get "/api/v1/search" do |env|
end
get "/api/v1/playlists/:plid" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
env.response.content_type = "application/json"
plid = env.params.url["plid"]
@@ -3160,14 +3275,14 @@ get "/api/v1/playlists/:plid" do |env|
end
begin
- playlist = fetch_playlist(plid)
+ playlist = fetch_playlist(plid, locale)
rescue ex
error_message = {"error" => "Playlist is empty"}.to_json
halt env, status_code: 500, response: error_message
end
begin
- videos = fetch_playlist_videos(plid, page, playlist.video_count, continuation)
+ videos = fetch_playlist_videos(plid, page, playlist.video_count, continuation, locale)
rescue ex
videos = [] of PlaylistVideo
end
@@ -3241,6 +3356,8 @@ get "/api/v1/playlists/:plid" do |env|
end
get "/api/v1/mixes/:rdid" do |env|
+ locale = LOCALES[env.get("locale").as(String)]?
+
env.response.content_type = "application/json"
rdid = env.params.url["rdid"]
@@ -3252,7 +3369,7 @@ get "/api/v1/mixes/:rdid" do |env|
format ||= "json"
begin
- mix = fetch_mix(rdid, continuation)
+ mix = fetch_mix(rdid, continuation, locale: locale)
if !rdid.ends_with? continuation
mix = fetch_mix(rdid, mix.videos[1].id)
diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr
index cfe389f1..ba17ff07 100644
--- a/src/invidious/channels.cr
+++ b/src/invidious/channels.cr
@@ -28,7 +28,7 @@ def get_channel(id, db, refresh = true, pull_all_videos = true)
channel = db.query_one("SELECT * FROM channels WHERE id = $1", id, as: InvidiousChannel)
if refresh && Time.now - channel.updated > 10.minutes
- channel = fetch_channel(id, client, db, pull_all_videos)
+ channel = fetch_channel(id, client, db, pull_all_videos: pull_all_videos)
channel_array = channel.to_a
args = arg_array(channel_array)
@@ -36,7 +36,7 @@ def get_channel(id, db, refresh = true, pull_all_videos = true)
ON CONFLICT (id) DO UPDATE SET author = $2, updated = $3", channel_array)
end
else
- channel = fetch_channel(id, client, db, pull_all_videos)
+ channel = fetch_channel(id, client, db, pull_all_videos: pull_all_videos)
channel_array = channel.to_a
args = arg_array(channel_array)
@@ -46,13 +46,13 @@ def get_channel(id, db, refresh = true, pull_all_videos = true)
return channel
end
-def fetch_channel(ucid, client, db, pull_all_videos = true)
+def fetch_channel(ucid, client, db, pull_all_videos = true, locale = nil)
rss = client.get("/feeds/videos.xml?channel_id=#{ucid}").body
rss = XML.parse_html(rss)
author = rss.xpath_node(%q(//feed/title))
if !author
- raise "Deleted or invalid channel"
+ raise translate(locale, "Deleted or invalid channel")
end
author = author.content
@@ -223,7 +223,7 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "
return url
end
-def get_about_info(ucid)
+def get_about_info(ucid, locale)
client = make_client(YT_URL)
about = client.get("/channel/#{ucid}/about?disable_polymer=1&gl=US&hl=en")
@@ -234,14 +234,14 @@ def get_about_info(ucid)
about = XML.parse_html(about.body)
if about.xpath_node(%q(//div[contains(@class, "channel-empty-message")]))
- error_message = "This channel does not exist."
+ error_message = translate(locale, "This channel does not exist.")
raise error_message
end
if about.xpath_node(%q(//span[contains(@class,"qualified-channel-title-text")]/a)).try &.content.empty?
error_message = about.xpath_node(%q(//div[@class="yt-alert-content"])).try &.content.strip
- error_message ||= "Could not get channel info."
+ error_message ||= translate(locale, "Could not get channel info.")
raise error_message
end
diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr
index 9f46b715..ad759468 100644
--- a/src/invidious/comments.cr
+++ b/src/invidious/comments.cr
@@ -56,7 +56,7 @@ class RedditListing
})
end
-def fetch_youtube_comments(id, continuation, proxies, format)
+def fetch_youtube_comments(id, continuation, proxies, format, locale)
client = make_client(YT_URL)
html = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
headers = HTTP::Headers.new
@@ -133,7 +133,7 @@ def fetch_youtube_comments(id, continuation, proxies, format)
response = JSON.parse(response.body)
if !response["response"]["continuationContents"]?
- raise "Could not fetch comments"
+ raise translate(locale, "Could not fetch comments")
end
response = response["response"]["continuationContents"]
@@ -214,7 +214,7 @@ def fetch_youtube_comments(id, continuation, proxies, format)
json.field "content", content
json.field "contentHtml", content_html
json.field "published", published.to_unix
- json.field "publishedText", "#{recode_date(published)} ago"
+ json.field "publishedText", translate(locale, "`x` ago", recode_date(published))
json.field "likeCount", node_comment["likeCount"]
json.field "commentId", node_comment["commentId"]
@@ -250,7 +250,7 @@ def fetch_youtube_comments(id, continuation, proxies, format)
if format == "html"
comments = JSON.parse(comments)
- content_html = template_youtube_comments(comments)
+ content_html = template_youtube_comments(comments, locale)
comments = JSON.build do |json|
json.object do
@@ -296,7 +296,7 @@ def fetch_reddit_comments(id)
return comments, thread
end
-def template_youtube_comments(comments)
+def template_youtube_comments(comments, locale)
html = ""
root = comments["comments"].as_a
@@ -308,7 +308,7 @@ def template_youtube_comments(comments)
@@ -328,7 +328,7 @@ def template_youtube_comments(comments)
#{child["author"]}
#{child["contentHtml"]}
- #{recode_date(Time.unix(child["published"].as_i64))} ago
+ #{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64)))}
|
#{number_with_separator(child["likeCount"])}
@@ -344,7 +344,7 @@ def template_youtube_comments(comments)
Load more
+ onclick="get_youtube_replies(this, true)">#{translate(locale, "Load more")}
@@ -354,7 +354,7 @@ def template_youtube_comments(comments)
return html
end
-def template_reddit_comments(root)
+def template_reddit_comments(root, locale)
html = ""
root.each do |child|
if child.data.is_a?(RedditComment)
@@ -366,15 +366,15 @@ def template_reddit_comments(root)
replies_html = ""
if child.replies.is_a?(RedditThing)
replies = child.replies.as(RedditThing)
- replies_html = template_reddit_comments(replies.data.as(RedditListing).children)
+ replies_html = template_reddit_comments(replies.data.as(RedditListing).children, locale)
end
content = <<-END_HTML
[ - ]
#{author}
- #{number_with_separator(score)} points
- #{recode_date(child.created_utc)} ago
+ #{translate(locale, "`x` points", number_with_separator(score))}
+ #{translate(locale, "`x` ago", recode_date(child.created_utc))}
#{body_html}
diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr
new file mode 100644
index 00000000..e79004d9
--- /dev/null
+++ b/src/invidious/helpers/i18n.cr
@@ -0,0 +1,23 @@
+def load_locale(name)
+ return JSON.parse(File.read("locales/#{name}.json")).as_h
+end
+
+def translate(locale : Hash(String, JSON::Any) | Nil, translation : String, text : String | Nil = nil)
+ if !locale
+ return translation
+ end
+
+ # if !locale[translation]?
+ # puts "Could not find translation for #{translation}"
+ # end
+
+ if locale[translation]? && !locale[translation].as_s.empty?
+ translation = locale[translation].as_s
+ end
+
+ if text
+ translation = translation.gsub("`x`", text)
+ end
+
+ return translation
+end
diff --git a/src/invidious/mixes.cr b/src/invidious/mixes.cr
index 688a8622..a56f468a 100644
--- a/src/invidious/mixes.cr
+++ b/src/invidious/mixes.cr
@@ -18,7 +18,7 @@ class Mix
})
end
-def fetch_mix(rdid, video_id, cookies = nil)
+def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
client = make_client(YT_URL)
headers = HTTP::Headers.new
headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
@@ -32,11 +32,11 @@ def fetch_mix(rdid, video_id, cookies = nil)
if yt_data
yt_data = JSON.parse(yt_data["data"].rchop(";"))
else
- raise "Could not create mix."
+ raise translate(locale, "Could not create mix.")
end
if !yt_data["contents"]["twoColumnWatchNextResults"]["playlist"]?
- raise "Could not create mix."
+ raise translate(locale, "Could not create mix.")
end
playlist = yt_data["contents"]["twoColumnWatchNextResults"]["playlist"]["playlist"]
@@ -70,7 +70,7 @@ def fetch_mix(rdid, video_id, cookies = nil)
end
if !cookies
- next_page = fetch_mix(rdid, videos[-1].id, response.cookies)
+ next_page = fetch_mix(rdid, videos[-1].id, response.cookies, locale)
videos += next_page.videos
end
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index c8e44c1b..4dbbf5da 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -26,7 +26,7 @@ class Playlist
})
end
-def fetch_playlist_videos(plid, page, video_count, continuation = nil)
+def fetch_playlist_videos(plid, page, video_count, continuation = nil, locale = nil)
client = make_client(YT_URL)
if continuation
@@ -48,7 +48,7 @@ def fetch_playlist_videos(plid, page, video_count, continuation = nil)
response = client.get(url)
response = JSON.parse(response.body)
if !response["content_html"]? || response["content_html"].as_s.empty?
- raise "Playlist is empty"
+ raise translate(locale, "Playlist is empty")
end
document = XML.parse_html(response["content_html"].as_s)
@@ -105,14 +105,14 @@ def extract_playlist(plid, nodeset, index)
end
videos << PlaylistVideo.new(
- title,
- id,
- author,
- ucid,
- length_seconds,
- Time.now,
- [plid],
- index + offset,
+ title: title,
+ id: id,
+ author: author,
+ ucid: ucid,
+ length_seconds: length_seconds,
+ published: Time.now,
+ playlists: [plid],
+ index: index + offset,
)
end
@@ -155,7 +155,7 @@ def produce_playlist_url(id, index)
return url
end
-def fetch_playlist(plid)
+def fetch_playlist(plid, locale)
client = make_client(YT_URL)
if plid.starts_with? "UC"
@@ -164,7 +164,7 @@ def fetch_playlist(plid)
response = client.get("/playlist?list=#{plid}&hl=en&disable_polymer=1")
if response.status_code != 200
- raise "Invalid playlist."
+ raise translate(locale, "Invalid playlist.")
end
body = response.body.gsub(%(
@@ -175,7 +175,7 @@ def fetch_playlist(plid)
title = document.xpath_node(%q(//h1[@class="pl-header-title"]))
if !title
- raise "Playlist does not exist."
+ raise translate(locale, "Playlist does not exist.")
end
title = title.content.strip(" \n")
diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr
index b8ef8186..453558d8 100644
--- a/src/invidious/trending.cr
+++ b/src/invidious/trending.cr
@@ -1,4 +1,4 @@
-def fetch_trending(trending_type, proxies, region)
+def fetch_trending(trending_type, proxies, region, locale)
client = make_client(YT_URL)
headers = HTTP::Headers.new
headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"
@@ -16,7 +16,7 @@ def fetch_trending(trending_type, proxies, region)
if yt_data
yt_data = JSON.parse(yt_data["data"].rchop(";"))
else
- raise "Could not pull trending pages."
+ raise translate(locale, "Could not pull trending pages.")
end
tabs = yt_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][0]["tabRenderer"]["content"]["sectionListRenderer"]["subMenu"]["channelListSubMenuRenderer"]["contents"].as_a
diff --git a/src/invidious/users.cr b/src/invidious/users.cr
index ccd36db5..38799a74 100644
--- a/src/invidious/users.cr
+++ b/src/invidious/users.cr
@@ -29,20 +29,25 @@ class User
end
DEFAULT_USER_PREFERENCES = Preferences.from_json({
- "video_loop" => false,
- "autoplay" => false,
- "speed" => 1.0,
- "quality" => "hd720",
- "volume" => 100,
- "comments" => ["youtube", ""],
- "captions" => ["", "", ""],
- "related_videos" => true,
- "dark_mode" => false,
- "thin_mode" => false,
- "max_results" => 40,
- "sort" => "published",
- "latest_only" => false,
- "unseen_only" => false,
+ "video_loop" => false,
+ "autoplay" => false,
+ "continue" => false,
+ "listen" => false,
+ "speed" => 1.0,
+ "quality" => "hd720",
+ "volume" => 100,
+ "comments" => ["youtube", ""],
+ "captions" => ["", "", ""],
+ "related_videos" => true,
+ "redirect_feed" => false,
+ "locale" => "en-US",
+ "dark_mode" => false,
+ "thin_mode" => false,
+ "max_results" => 40,
+ "sort" => "published",
+ "latest_only" => false,
+ "unseen_only" => false,
+ "notifications_only" => false,
}.to_json)
class Preferences
@@ -113,6 +118,10 @@ class Preferences
type: Bool,
default: false,
},
+ locale: {
+ type: String,
+ default: "en-US",
+ },
})
end
@@ -217,13 +226,13 @@ def create_response(user_id, operation, key, db, expire = 6.hours)
return challenge, token
end
-def validate_response(challenge, token, user_id, operation, key, db)
+def validate_response(challenge, token, user_id, operation, key, db, locale)
if !challenge
- raise "Hidden field \"challenge\" is a required field"
+ raise translate(locale, "Hidden field \"challenge\" is a required field")
end
if !token
- raise "Hidden field \"token\" is a required field"
+ raise translate(locale, "Hidden field \"token\" is a required field")
end
challenge = Base64.decode_string(challenge)
@@ -233,7 +242,7 @@ def validate_response(challenge, token, user_id, operation, key, db)
expire = expire.to_i?
expire ||= 0
else
- raise "Invalid challenge"
+ raise translate(locale, "Invalid challenge")
end
challenge = OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge)
@@ -242,23 +251,23 @@ def validate_response(challenge, token, user_id, operation, key, db)
if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool)
db.exec("DELETE FROM nonces * WHERE nonce = $1", nonce)
else
- raise "Invalid token"
+ raise translate(locale, "Invalid token")
end
if challenge != token
- raise "Invalid token"
+ raise translate(locale, "Invalid token")
end
if challenge_operation != operation
- raise "Invalid token"
+ raise translate(locale, "Invalid token")
end
if challenge_user_id != user_id
- raise "Invalid user"
+ raise translate(locale, "Invalid user")
end
if expire < Time.now.to_unix
- raise "Token is expired, please try again"
+ raise translate(locale, "Token is expired, please try again")
end
end
diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr
index 25cf191d..38e7bc1b 100644
--- a/src/invidious/views/channel.ecr
+++ b/src/invidious/views/channel.ecr
@@ -19,14 +19,14 @@
">
- Unsubscribe | <%= number_to_short_text(sub_count) %>
+ <%= translate(locale, "Unsubscribe") %> | <%= number_to_short_text(sub_count) %>
<% else %>
">
- Subscribe | <%= number_to_short_text(sub_count) %>
+ <%= translate(locale, "Subscribe") %> | <%= number_to_short_text(sub_count) %>
<% end %>
@@ -34,7 +34,7 @@
">
- Login to subscribe to <%= author %>
+ <%= translate(locale, "Login to subscribe to `x`", author) %>
<% end %>
@@ -42,7 +42,7 @@
@@ -51,10 +51,10 @@
<% {"newest", "oldest", "popular"}.each do |sort| %>
@@ -78,13 +78,17 @@
@@ -105,7 +109,7 @@ function subscribe() {
if (xhr.status == 200) {
subscribe_button = document.getElementById("subscribe");
subscribe_button.onclick = unsubscribe;
- subscribe_button.innerHTML = '
Unsubscribe | <%= number_to_short_text(sub_count) %>'
+ subscribe_button.innerHTML = '
<%= translate(locale, "Unsubscribe") %> | <%= number_to_short_text(sub_count) %>'
}
}
}
@@ -124,7 +128,7 @@ function unsubscribe() {
if (xhr.status == 200) {
subscribe_button = document.getElementById("subscribe");
subscribe_button.onclick = subscribe;
- subscribe_button.innerHTML = '
Subscribe | <%= number_to_short_text(sub_count) %>'
+ subscribe_button.innerHTML = '
<%= translate(locale, "Subscribe") %> | <%= number_to_short_text(sub_count) %>'
}
}
}
diff --git a/src/invidious/views/clear_watch_history.ecr b/src/invidious/views/clear_watch_history.ecr
index 9a726a68..ede5e287 100644
--- a/src/invidious/views/clear_watch_history.ecr
+++ b/src/invidious/views/clear_watch_history.ecr
@@ -1,13 +1,21 @@
+<% content_for "header" do %>
+
<%= translate(locale, "Clear watch history") %> - Invidious
+<% end %>
+
diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr
index f3ed775f..cb7c1276 100644
--- a/src/invidious/views/components/player.ecr
+++ b/src/invidious/views/components/player.ecr
@@ -27,12 +27,12 @@
<% end %>
<% preferred_captions.each_with_index do |caption, i| %>
-