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)

View #{child["replies"]["replyCount"]} replies + onclick="get_youtube_replies(this)">#{translate(locale, "View `x` replies", child["replies"]["replyCount"].to_s)}

@@ -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| %>
<% if sort_by == sort %> - <%= sort %> + <%= translate(locale, sort) %> <% else %> - <%= sort %> + <%= translate(locale, sort) %> <% end %>
@@ -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 %> +
- Clear watch history? + <%= translate(locale, "Clear watch history?") %>
- +
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index b38e7a5d..8b013907 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -11,8 +11,8 @@ <% end %>

<%= item.author %>

-

<%= number_with_separator(item.subscriber_count) %> subscribers

-

<%= number_with_separator(item.video_count) %> videos

+

<%= translate(locale, "`x` subscribers", number_with_separator(item.subscriber_count)) %>

+

<%= translate(locale, "`x` videos", number_with_separator(item.video_count)) %>

<%= item.description_html %>
<% when SearchPlaylist %> <% if item.id.starts_with? "RD" %> @@ -59,14 +59,14 @@

<%= item.title %>

<% if item.responds_to?(:live_now) && item.live_now %> -

LIVE

+

<%= translate(locale, "LIVE") %>

<% end %>

<%= item.author %>

<% if Time.now - item.published > 1.minute %> -
Shared <%= recode_date(item.published) %> ago
+
<%= translate(locale, "Shared `x` ago", recode_date(item.published)) %>
<% end %> <% else %> <% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %> @@ -93,14 +93,14 @@ <% end %>

<%= item.title %>

<% if item.responds_to?(:live_now) && item.live_now %> -

LIVE

+

<%= translate(locale, "LIVE") %>

<% end %>

<%= item.author %>

<% if Time.now - item.published > 1.minute %> -
Shared <%= recode_date(item.published) %> ago
+
<%= translate(locale, "Shared `x` ago", recode_date(item.published)) %>
<% end %> <% 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| %> - " label="<%= caption.name.simpleText %>" <% if i == 0 %>default<% end %>> <% end %> <% captions.each do |caption| %> - " label="<%= caption.name.simpleText %>"> <% end %> <% end %> diff --git a/src/invidious/views/data_control.ecr b/src/invidious/views/data_control.ecr index 0efcba28..2a563e02 100644 --- a/src/invidious/views/data_control.ecr +++ b/src/invidious/views/data_control.ecr @@ -1,54 +1,57 @@ <% content_for "header" do %> -Import and Export Data - Invidious +<%= translate(locale, "Import and Export Data") %> - Invidious <% end %>
- Import + <%= translate(locale, "Import") %>
- +
- +
- +
- +
- +
- Export + <%= translate(locale, "Export") %>
diff --git a/src/invidious/views/delete_account.ecr b/src/invidious/views/delete_account.ecr index 8f2b61d6..7cc8de9b 100644 --- a/src/invidious/views/delete_account.ecr +++ b/src/invidious/views/delete_account.ecr @@ -1,13 +1,21 @@ +<% content_for "header" do %> +<%= translate(locale, "Delete account") %> - Invidious +<% end %> +
- Delete account? + <%= translate(locale, "Delete account?") %>
- +
diff --git a/src/invidious/views/history.ecr b/src/invidious/views/history.ecr index 3830e9c4..e3cbd7eb 100644 --- a/src/invidious/views/history.ecr +++ b/src/invidious/views/history.ecr @@ -1,14 +1,14 @@ <% content_for "header" do %> -History - Invidious +<%= translate(locale, "History") %> - Invidious <% end %>
-

<%= user.watched.size %> videos

+

<%= translate(locale, "`x` videos", %(#{user.watched.size})) %>

@@ -69,13 +69,17 @@ function mark_unwatched(target) {
<% if watched.size >= limit %> - Next page + + <%= translate(locale, "Next page") %> + <% end %>
diff --git a/src/invidious/views/index.ecr b/src/invidious/views/index.ecr index cd88d540..ca5d3ae8 100644 --- a/src/invidious/views/index.ecr +++ b/src/invidious/views/index.ecr @@ -1,5 +1,5 @@ <% content_for "header" do %> - +"> Invidious <% end %> diff --git a/src/invidious/views/licenses.ecr b/src/invidious/views/licenses.ecr index f211afce..ec24ed63 100644 --- a/src/invidious/views/licenses.ecr +++ b/src/invidious/views/licenses.ecr @@ -7,7 +7,7 @@ -

JavaScript license information

+

<%= translate(locale, "JavaScript license information") %>

@@ -33,7 +33,7 @@ @@ -47,7 +47,7 @@ @@ -61,7 +61,7 @@ @@ -75,7 +75,7 @@ @@ -89,7 +89,7 @@ @@ -103,7 +103,7 @@ @@ -117,7 +117,7 @@ @@ -131,7 +131,7 @@ @@ -145,7 +145,7 @@
@@ -19,7 +19,7 @@ - source + <%= translate(locale, "source") %>
- source + <%= translate(locale, "source") %>
- source + <%= translate(locale, "source") %>
- source + <%= translate(locale, "source") %>
- source + <%= translate(locale, "source") %>
- source + <%= translate(locale, "source") %>
- source + <%= translate(locale, "source") %>
- source + <%= translate(locale, "source") %>
- source + <%= translate(locale, "source") %>
- source + <%= translate(locale, "source") %>
diff --git a/src/invidious/views/login.ecr b/src/invidious/views/login.ecr index 55f4c848..f4b0ce96 100644 --- a/src/invidious/views/login.ecr +++ b/src/invidious/views/login.ecr @@ -1,5 +1,5 @@ <% content_for "header" do %> -Login - Invidious +<%= translate(locale, "Login") %> - Invidious <% end %>
@@ -8,31 +8,37 @@

<% if account_type == "invidious" %>
- + - + <% if captcha_type == "image" %> - + <% else %> <% text_captcha.not_nil![:tokens].each_with_index do |token, i| %> @@ -43,29 +49,31 @@ <% end %> - - + +
<% elsif account_type == "google" %>
- + - + <% if tfa %> - + <% end %> - +
<% end %> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 018e59f7..e6775e15 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -35,13 +35,17 @@
<% if videos.size == 100 %> - Next page + + <%= translate(locale, "Next page") %> + <% end %>
diff --git a/src/invidious/views/popular.ecr b/src/invidious/views/popular.ecr index 6cd3d8d6..8ce1c161 100644 --- a/src/invidious/views/popular.ecr +++ b/src/invidious/views/popular.ecr @@ -1,3 +1,7 @@ +<% content_for "header" do %> +<%= translate(locale, "Popular") %> - Invidious +<% end %> +
<% popular_videos.each_slice(4) do |slice| %> <% slice.each do |item| %> diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index b55e4048..7a01004b 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -1,5 +1,5 @@ <% content_for "header" do %> -Preferences - Invidious +<%= translate(locale, "Preferences") %> - Invidious <% end %>