diff options
28 files changed, 502 insertions, 272 deletions
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("<br>", "\n") description = description.gsub("<br/>", "\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) <div class="pure-u-23-24"> <p> <a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}" - onclick="get_youtube_replies(this)">View #{child["replies"]["replyCount"]} replies</a> + onclick="get_youtube_replies(this)">#{translate(locale, "View `x` replies", child["replies"]["replyCount"].to_s)}</a> </p> </div> </div> @@ -328,7 +328,7 @@ def template_youtube_comments(comments) <a href="#{child["authorUrl"]}">#{child["author"]}</a> </b> <p style="white-space:pre-wrap">#{child["contentHtml"]}</p> - #{recode_date(Time.unix(child["published"].as_i64))} ago + #{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64)))} | <i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])} </p> @@ -344,7 +344,7 @@ def template_youtube_comments(comments) <div class="pure-u-1"> <p> <a href="javascript:void(0)" data-continuation="#{comments["continuation"]}" - onclick="get_youtube_replies(this, true)">Load more</a> + onclick="get_youtube_replies(this, true)">#{translate(locale, "Load more")}</a> </p> </div> </div> @@ -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 <p> <a href="javascript:void(0)" onclick="toggle_parent(this)">[ - ]</a> <b><a href="https://www.reddit.com/user/#{author}">#{author}</a></b> - #{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))} </p> <div> #{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 @@ <p> <a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary" href="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>"> - <b>Unsubscribe | <%= number_to_short_text(sub_count) %></b> + <b><%= translate(locale, "Unsubscribe") %> | <%= number_to_short_text(sub_count) %></b> </a> </p> <% else %> <p> <a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary" href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>"> - <b>Subscribe | <%= number_to_short_text(sub_count) %></b> + <b><%= translate(locale, "Subscribe") %> | <%= number_to_short_text(sub_count) %></b> </a> </p> <% end %> @@ -34,7 +34,7 @@ <p> <a id="subscribe" class="pure-button pure-button-primary" href="/login?referer=<%= env.get("current_page") %>"> - <b>Login to subscribe to <%= author %></b> + <b><%= translate(locale, "Login to subscribe to `x`", author) %></b> </a> </p> <% end %> @@ -42,7 +42,7 @@ <div class="pure-g h-box"> <div class="pure-u-1-3"> - <a href="https://www.youtube.com/channel/<%= ucid %>">View channel on YouTube</a> + <a href="https://www.youtube.com/channel/<%= ucid %>"><%= translate(locale, "View channel on YouTube") %></a> </div> <div class="pure-u-1-3"> </div> @@ -51,10 +51,10 @@ <% {"newest", "oldest", "popular"}.each do |sort| %> <div class="pure-u-1 pure-md-1-3"> <% if sort_by == sort %> - <b><%= sort %></b> + <b><%= translate(locale, sort) %></b> <% else %> <a href="/channel/<%= ucid %>?page=<%= page %>&sort_by=<%= sort %>"> - <%= sort %> + <%= translate(locale, sort) %> </a> <% end %> </div> @@ -78,13 +78,17 @@ <div class="pure-g h-box"> <div class="pure-u-1 pure-u-md-1-5"> <% if page >= 2 %> - <a href="/channel/<%= ucid %>?page=<%= page - 1 %><% if sort_by != "newest" %>&sort_by=<%= sort_by %><% end %>">Previous page</a> + <a href="/channel/<%= ucid %>?page=<%= page - 1 %><% if sort_by != "newest" %>&sort_by=<%= sort_by %><% end %>"> + <%= translate(locale, "Previous page") %> + </a> <% end %> </div> <div class="pure-u-1 pure-u-md-3-5"></div> <div style="text-align:right;" class="pure-u-1 pure-u-md-1-5"> <% if count == 60 %> - <a href="/channel/<%= ucid %>?page=<%= page + 1 %><% if sort_by != "newest" %>&sort_by=<%= sort_by %><% end %>">Next page</a> + <a href="/channel/<%= ucid %>?page=<%= page + 1 %><% if sort_by != "newest" %>&sort_by=<%= sort_by %><% end %>"> + <%= translate(locale, "Next page") %> + </a> <% end %> </div> </div> @@ -105,7 +109,7 @@ function subscribe() { if (xhr.status == 200) { subscribe_button = document.getElementById("subscribe"); subscribe_button.onclick = unsubscribe; - subscribe_button.innerHTML = '<b>Unsubscribe | <%= number_to_short_text(sub_count) %></b>' + subscribe_button.innerHTML = '<b><%= translate(locale, "Unsubscribe") %> | <%= number_to_short_text(sub_count) %></b>' } } } @@ -124,7 +128,7 @@ function unsubscribe() { if (xhr.status == 200) { subscribe_button = document.getElementById("subscribe"); subscribe_button.onclick = subscribe; - subscribe_button.innerHTML = '<b>Subscribe | <%= number_to_short_text(sub_count) %></b>' + subscribe_button.innerHTML = '<b><%= translate(locale, "Subscribe") %> | <%= number_to_short_text(sub_count) %></b>' } } } 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 %> +<title><%= translate(locale, "Clear watch history") %> - Invidious</title> +<% end %> + <div class="h-box"> <form class="pure-form pure-form-aligned" action="/clear_watch_history?referer=<%= URI.escape(referer) %>" method="post"> - <legend>Clear watch history?</legend> + <legend><%= translate(locale, "Clear watch history?") %></legend> <div class="pure-g"> <div class="pure-u-1-2"> - <button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary">Yes</button> + <button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary"> + <%= translate(locale, "Yes") %> + </button> </div> <div class="pure-u-1-2"> - <a class="pure-button" href="<%= referer %>">No</a> + <a class="pure-button" href="<%= referer %>"> + <%= translate(locale, "No") %> + </a> </div> </div> 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 %> <p><%= item.author %></p> </a> - <p><%= number_with_separator(item.subscriber_count) %> subscribers</p> - <p><%= number_with_separator(item.video_count) %> videos</p> + <p><%= translate(locale, "`x` subscribers", number_with_separator(item.subscriber_count)) %></p> + <p><%= translate(locale, "`x` videos", number_with_separator(item.video_count)) %></p> <h5><%= item.description_html %></h5> <% when SearchPlaylist %> <% if item.id.starts_with? "RD" %> @@ -59,14 +59,14 @@ <p><%= item.title %></p> </a> <% if item.responds_to?(:live_now) && item.live_now %> - <p>LIVE</p> + <p><%= translate(locale, "LIVE") %></p> <% end %> <p> <b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b> </p> <% if Time.now - item.published > 1.minute %> - <h5>Shared <%= recode_date(item.published) %> ago</h5> + <h5><%= translate(locale, "Shared `x` ago", recode_date(item.published)) %></h5> <% end %> <% else %> <% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %> @@ -93,14 +93,14 @@ <% end %> <p><a href="/watch?v=<%= item.id %>"><%= item.title %></a></p> <% if item.responds_to?(:live_now) && item.live_now %> - <p>LIVE</p> + <p><%= translate(locale, "LIVE") %></p> <% end %> <p> <b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b> </p> <% if Time.now - item.published > 1.minute %> - <h5>Shared <%= recode_date(item.published) %> ago</h5> + <h5><%= translate(locale, "Shared `x` ago", recode_date(item.published)) %></h5> <% end %> <% end %> </div> 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| %> - <track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name.simpleText %>" + <track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name.simpleText %>&hl=<%= env.get("locale").as(String) %>" label="<%= caption.name.simpleText %>" <% if i == 0 %>default<% end %>> <% end %> <% captions.each do |caption| %> - <track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name.simpleText %>" + <track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name.simpleText %>&hl=<%= env.get("locale").as(String) %>" 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 %> -<title>Import and Export Data - Invidious</title> +<title><%= translate(locale, "Import and Export Data") %> - Invidious</title> <% end %> <div class="h-box"> <form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= referer %>" method="post"> <fieldset> - <legend>Import</legend> + <legend><%= translate(locale, "Import") %></legend> <div class="pure-control-group"> - <label for="import_youtube">Import Invidious data</label> + <label for="import_youtube"><%= translate(locale, "Import Invidious data") %></label> <input type="file" id="import_invidious" name="import_invidious"> </div> <div class="pure-control-group"> - <label for="import_youtube">Import <a rel="noopener" target="_blank" - href="https://support.google.com/youtube/answer/6224202?hl=en-GB">YouTube subscriptions</a></label> + <label for="import_youtube"> + <a rel="noopener" target="_blank" href="https://support.google.com/youtube/answer/6224202?hl=en"> + <%= translate(locale, "Import YouTube subscriptions") %> + </a> + </label> <input type="file" id="import_youtube" name="import_youtube"> </div> <div class="pure-control-group"> - <label for="import_freetube">Import Freetube subscriptions (.db)</label> + <label for="import_freetube"><%= translate(locale, "Import Freetube subscriptions (.db)") %></label> <input type="file" id="import_freetube" name="import_freetube"> </div> <div class="pure-control-group"> - <label for="import_newpipe_subscriptions">Import NewPipe subscriptions (.json)</label> + <label for="import_newpipe_subscriptions"><%= translate(locale, "Import NewPipe subscriptions (.json)") %></label> <input type="file" id="import_newpipe_subscriptions" name="import_newpipe_subscriptions"> </div> <div class="pure-control-group"> - <label for="import_newpipe">Import NewPipe data (.zip)</label> + <label for="import_newpipe"><%= translate(locale, "Import NewPipe data (.zip)") %></label> <input type="file" id="import_newpipe" name="import_newpipe"> </div> <div class="pure-controls"> - <button type="submit" class="pure-button pure-button-primary">Import</button> + <button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Import") %></button> </div> - <legend>Export</legend> + <legend><%= translate(locale, "Export") %></legend> <div class="pure-control-group"> - <a href="/subscription_manager?action_takeout=1">Export subscriptions as OPML</a> + <a href="/subscription_manager?action_takeout=1"><%= translate(locale, "Export subscriptions as OPML") %></a> </div> <div class="pure-control-group"> - <a href="/subscription_manager?action_takeout=1&format=newpipe">Export subscriptions as OPML (for NewPipe & FreeTube)</a> + <a href="/subscription_manager?action_takeout=1&format=newpipe"><%= translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %></a> </div> <div class="pure-control-group"> - <a href="/subscription_manager?action_takeout=1&format=json">Export data as JSON</a> + <a href="/subscription_manager?action_takeout=1&format=json"><%= translate(locale, "Export data as JSON") %></a> </div> </fieldset> </form> 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 %> +<title><%= translate(locale, "Delete account") %> - Invidious</title> +<% end %> + <div class="h-box"> <form class="pure-form pure-form-aligned" action="/delete_account?referer=<%= URI.escape(referer) %>" method="post"> - <legend>Delete account?</legend> + <legend><%= translate(locale, "Delete account?") %></legend> <div class="pure-g"> <div class="pure-u-1-2"> - <button type="submit" name="submit" value="delete_account" class="pure-button pure-button-primary">Yes</button> + <button type="submit" name="submit" value="delete_account" class="pure-button pure-button-primary"> + <%= translate(locale, "Yes") %> + </button> </div> <div class="pure-u-1-2"> - <a class="pure-button" href="<%= referer %>">No</a> + <a class="pure-button" href="<%= referer %>"> + <%= translate(locale, "No") %> + </a> </div> </div> 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 %> -<title>History - Invidious</title> +<title><%= translate(locale, "History") %> - Invidious</title> <% end %> <div class="pure-g h-box"> <div class="pure-u-2-3"> - <h3><span id="count"><%= user.watched.size %></span> videos</h3> + <h3><%= translate(locale, "`x` videos", %(<span id="count">#{user.watched.size}</span>)) %></h3> </div> <div class="pure-u-1-3" style="text-align:right;"> <h3> - <a href="/clear_watch_history">Clear watch history</a> + <a href="/clear_watch_history"><%= translate(locale, "Clear watch history") %></a> </h3> </div> </div> @@ -69,13 +69,17 @@ function mark_unwatched(target) { <div class="pure-g h-box"> <div class="pure-u-1 pure-u-md-1-5"> <% if page >= 2 %> - <a href="/feed/history?page=<%= page - 1 %>">Previous page</a> + <a href="/feed/history?page=<%= page - 1 %>"> + <%= translate(locale, "Previous page") %> + </a> <% end %> </div> <div class="pure-u-1 pure-u-md-3-5"></div> <div style="text-align:right;" class="pure-u-1 pure-u-md-1-5"> <% if watched.size >= limit %> - <a href="/feed/history?page=<%= page + 1 %>">Next page</a> + <a href="/feed/history?page=<%= page + 1 %>"> + <%= translate(locale, "Next page") %> + </a> <% end %> </div> </div> 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 %> -<meta name="description" content="An alternative front-end to YouTube"> +<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> <title>Invidious</title> <% 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 @@ </head> <body> - <h1>JavaScript license information</h1> + <h1><%= translate(locale, "JavaScript license information") %></h1> <table id="jslicense-labels1"> <tr> <td> @@ -19,7 +19,7 @@ </td> <td> - <a href="https://unpkg.com/dashjs@2.9.0/dist/dash.mediaplayer.debug.js">source</a> + <a href="https://unpkg.com/dashjs@2.9.0/dist/dash.mediaplayer.debug.js"><%= translate(locale, "source") %></a> </td> </tr> @@ -33,7 +33,7 @@ </td> <td> - <a href="/js/silvermine-videojs-quality-selector.js">source</a> + <a href="/js/silvermine-videojs-quality-selector.js"><%= translate(locale, "source") %></a> </td> </tr> @@ -47,7 +47,7 @@ </td> <td> - <a href="https://unpkg.com/video.js@6.12.1/dist/video.js">source</a> + <a href="https://unpkg.com/video.js@6.12.1/dist/video.js"><%= translate(locale, "source") %></a> </td> </tr> @@ -61,7 +61,7 @@ </td> <td> - <a href="https://unpkg.com/videojs-contrib-quality-levels@2.0.7/dist/videojs-contrib-quality-levels.js">source</a> + <a href="https://unpkg.com/videojs-contrib-quality-levels@2.0.7/dist/videojs-contrib-quality-levels.js"><%= translate(locale, "source") %></a> </td> </tr> @@ -75,7 +75,7 @@ </td> <td> - <a href="https://unpkg.com/videojs-contrib-dash@2.8.2/dist/videojs-dash.js">source</a> + <a href="https://unpkg.com/videojs-contrib-dash@2.8.2/dist/videojs-dash.js"><%= translate(locale, "source") %></a> </td> </tr> @@ -89,7 +89,7 @@ </td> <td> - <a href="https://unpkg.com/@videojs/http-streaming@1.2.2/dist/videojs-http-streaming.js">source</a> + <a href="https://unpkg.com/@videojs/http-streaming@1.2.2/dist/videojs-http-streaming.js"><%= translate(locale, "source") %></a> </td> </tr> @@ -103,7 +103,7 @@ </td> <td> - <a href="https://unpkg.com/videojs-markers@1.0.1/dist/videojs-markers.js">source</a> + <a href="https://unpkg.com/videojs-markers@1.0.1/dist/videojs-markers.js"><%= translate(locale, "source") %></a> </td> </tr> @@ -117,7 +117,7 @@ </td> <td> - <a href="https://unpkg.com/videojs-share@2.0.1/dist/videojs-share.js">source</a> + <a href="https://unpkg.com/videojs-share@2.0.1/dist/videojs-share.js"><%= translate(locale, "source") %></a> </td> </tr> @@ -131,7 +131,7 @@ </td> <td> - <a href="/js/videojs.hotkeys.js">source</a> + <a href="/js/videojs.hotkeys.js"><%= translate(locale, "source") %></a> </td> </tr> @@ -145,7 +145,7 @@ </td> <td> - <a href="/js/watch.js">source</a> + <a href="/js/watch.js"><%= translate(locale, "source") %></a> </td> </tr> </table> 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 %> -<title>Login - Invidious</title> +<title><%= translate(locale, "Login") %> - Invidious</title> <% end %> <div class="pure-g"> @@ -8,31 +8,37 @@ <div class="h-box"> <div class="pure-g"> <div class="pure-u-1-2"> - <a class="pure-button <% if account_type == "invidious" %>pure-button-disabled<% end %>" href="/login">Login/Register</a> + <a class="pure-button <% if account_type == "invidious" %>pure-button-disabled<% end %>" href="/login"> + <%= translate(locale, "Login/Register") %> + </a> </div> <div class="pure-u-1-2"> - <a class="pure-button <% if account_type == "google" %>pure-button-disabled<% end %>" href="/login?type=google">Login to Google</a> + <a class="pure-button <% if account_type == "google" %>pure-button-disabled<% end %>" href="/login?type=google"> + <%= translate(locale, "Login to Google") %> + </a> </div> </div> <hr> <% if account_type == "invidious" %> <form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>&type=invidious" method="post"> <fieldset> - <label for="email">User ID:</label> + <label for="email"><%= translate(locale, "User ID:") %></label> <input required class="pure-input-1" name="email" type="text" placeholder="User ID"> - <label for="password">Password:</label> + <label for="password"><%= translate(locale, "Password:") %></label> <input required class="pure-input-1" name="password" type="password" placeholder="Password"> <% if captcha_type == "image" %> <img style="width:100%" src='<%= captcha.not_nil![:image] %>'/> <input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>"> <input type="hidden" name="challenge" value="<%= captcha.not_nil![:challenge] %>"> - <label for="answer">Time (h:mm:ss):</label> + <label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label> <input required type="text" name="answer" type="text" placeholder="h:mm:ss"> <label> - <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious&captcha=text">Text CAPTCHA</a> + <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious&captcha=text"> + <%= translate(locale, "Text CAPTCHA") %> + </a> </label> <% else %> <% text_captcha.not_nil![:tokens].each_with_index do |token, i| %> @@ -43,29 +49,31 @@ <input required type="text" name="text_answer" type="text" placeholder="Answer"> <label> - <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious">Image CAPTCHA</a> + <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious"> + <%= translate(locale, "Image CAPTCHA") %> + </a> </label> <% end %> - <button type="submit" name="action" value="signin" class="pure-button pure-button-primary">Sign In</button> - <button type="submit" name="action" value="register" class="pure-button pure-button-primary">Register</button> + <button type="submit" name="action" value="signin" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button> + <button type="submit" name="action" value="register" class="pure-button pure-button-primary"><%= translate(locale, "Register") %></button> </fieldset> </form> <% elsif account_type == "google" %> <form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>" method="post"> <fieldset> - <label for="email">Email:</label> + <label for="email"><%= translate(locale, "Email:") %></label> <input required class="pure-input-1" name="email" type="email" placeholder="Email"> - <label for="password">Password:</label> + <label for="password"><%= translate(locale, "Password:") %></label> <input required class="pure-input-1" name="password" type="password" placeholder="Password"> <% if tfa %> - <label for="tfa">Google verification code:</label> + <label for="tfa"><%= translate(locale, "Google verification code:") %></label> <input required class="pure-input-1" name="tfa" type="text" placeholder="Google verification code"> <% end %> - <button type="submit" class="pure-button pure-button-primary">Sign In</button> + <button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button> </fieldset> </form> <% 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 @@ <div class="pure-g h-box"> <div class="pure-u-1 pure-u-md-1-5"> <% if page >= 2 %> - <a href="/playlist?list=<%= playlist.id %>&page=<%= page - 1 %>">Previous page</a> + <a href="/playlist?list=<%= playlist.id %>&page=<%= page - 1 %>"> + <%= translate(locale, "Previous page") %> + </a> <% end %> </div> <div class="pure-u-1 pure-u-md-3-5"></div> <div style="text-align:right;" class="pure-u-1 pure-u-md-1-5"> <% if videos.size == 100 %> - <a href="/playlist?list=<%= playlist.id %>&page=<%= page + 1 %>">Next page</a> + <a href="/playlist?list=<%= playlist.id %>&page=<%= page + 1 %>"> + <%= translate(locale, "Next page") %> + </a> <% end %> </div> </div> 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 %> +<title><%= translate(locale, "Popular") %> - Invidious</title> +<% end %> + <div class="pure-g"> <% 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 %> -<title>Preferences - Invidious</title> +<title><%= translate(locale, "Preferences") %> - Invidious</title> <% end %> <script> @@ -11,30 +11,30 @@ function update_value(element) { <div class="h-box"> <form class="pure-form pure-form-aligned" action="/preferences?referer=<%= referer %>" method="post"> <fieldset> - <legend>Player preferences</legend> + <legend><%= translate(locale, "Player preferences") %></legend> <div class="pure-control-group"> - <label for="video_loop">Always loop: </label> + <label for="video_loop"><%= translate(locale, "Always loop: ") %></label> <input name="video_loop" id="video_loop" type="checkbox" <% if user.preferences.video_loop %>checked<% end %>> </div> <div class="pure-control-group"> - <label for="autoplay">Autoplay: </label> + <label for="autoplay"><%= translate(locale, "Autoplay: ") %></label> <input name="autoplay" id="autoplay" type="checkbox" <% if user.preferences.autoplay %>checked<% end %>> </div> <div class="pure-control-group"> - <label for="continue">Autoplay next video: </label> + <label for="continue"><%= translate(locale, "Autoplay next video: ") %></label> <input name="continue" id="continue" type="checkbox" <% if user.preferences.continue %>checked<% end %>> </div> <div class="pure-control-group"> - <label for="listen">Listen by default: </label> + <label for="listen"><%= translate(locale, "Listen by default: ") %></label> <input name="listen" id="listen" type="checkbox" <% if user.preferences.listen %>checked<% end %>> </div> <div class="pure-control-group"> - <label for="speed">Default speed: </label> + <label for="speed"><%= translate(locale, "Default speed: ") %></label> <select name="speed" id="speed"> <% {2.0, 1.5, 1.0, 0.5}.each do |option| %> <option <% if user.preferences.speed == option %> selected <% end %>><%= option %></option> @@ -43,96 +43,105 @@ function update_value(element) { </div> <div class="pure-control-group"> - <label for="quality">Preferred video quality: </label> + <label for="quality"><%= translate(locale, "Preferred video quality: ") %></label> <select name="quality" id="quality"> <% {"dash", "hd720", "medium", "small"}.each do |option| %> - <option <% if user.preferences.quality == option %> selected <% end %>><%= option %></option> + <option value="<%= option %>" <% if user.preferences.quality == option %> selected <% end %>><%= translate(locale, option) %></option> <% end %> </select> </div> <div class="pure-control-group"> - <label for="volume">Player volume: </label> + <label for="volume"><%= translate(locale, "Player volume: ") %></label> <input name="volume" id="volume" oninput="update_value(this);" type="range" min="0" max="100" step="5" value="<%= user.preferences.volume %>"> <span class="pure-form-message-inline" id="volume-value"><%= user.preferences.volume %></span> </div> <div class="pure-control-group"> - <label for="comments_0">Default comments: </label> + <label for="comments_0"><%= translate(locale, "Default comments: ") %></label> <select name="comments_0" id="comments_0"> <% {"", "youtube", "reddit"}.each do |option| %> - <option <% if user.preferences.comments[0] == option %> selected <% end %>><%= option %></option> + <option value="<%= option %>" <% if user.preferences.comments[0] == option %> selected <% end %>><%= translate(locale, option) %></option> <% end %> </select> </div> <div class="pure-control-group"> - <label for="comments_1">Fallback comments: </label> + <label for="comments_1"><%= translate(locale, "Fallback comments: ") %></label> <select name="comments_1" id="comments_1"> <% {"", "youtube", "reddit"}.each do |option| %> - <option <% if user.preferences.comments[1] == option %> selected <% end %>><%= option %></option> + <option value="<%= option %>" <% if user.preferences.comments[1] == option %> selected <% end %>><%= translate(locale, option) %></option> <% end %> </select> </div> <div class="pure-control-group"> - <label for="captions_0">Default captions: </label> + <label for="captions_0"><%= translate(locale, "Default captions: ") %></label> <select class="pure-u-1-5" name="captions_0" id="captions_0"> <% CAPTION_LANGUAGES.each do |option| %> - <option <% if user.preferences.captions[0] == option %> selected <% end %>><%= option %></option> + <option value="<%= option %>" <% if user.preferences.captions[0] == option %> selected <% end %>><%= translate(locale, option) %></option> <% end %> </select> </div> <div class="pure-control-group"> - <label for="captions_fallback">Fallback captions: </label> + <label for="captions_fallback"><%= translate(locale, "Fallback captions: ") %></label> <select class="pure-u-1-5" name="captions_1" id="captions_1"> <% CAPTION_LANGUAGES.each do |option| %> - <option <% if user.preferences.captions[1] == option %> selected <% end %>><%= option %></option> + <option value="<%= option %>" <% if user.preferences.captions[1] == option %> selected <% end %>><%= translate(locale, option) %></option> <% end %> </select> <select class="pure-u-1-5" name="captions_2" id="captions_2"> <% CAPTION_LANGUAGES.each do |option| %> - <option <% if user.preferences.captions[2] == option %> selected <% end %>><%= option %></option> + <option value="<%= option %>" <% if user.preferences.captions[2] == option %> selected <% end %>><%= translate(locale, option) %></option> <% end %> </select> </div> <div class="pure-control-group"> - <label for="related_videos">Show related videos? </label> + <label for="related_videos"><%= translate(locale, "Show related videos? ") %></label> <input name="related_videos" id="related_videos" type="checkbox" <% if user.preferences.related_videos %>checked<% end %>> </div> - <legend>Visual preferences</legend> + <legend><%= translate(locale, "Visual preferences") %></legend> <div class="pure-control-group"> - <label for="dark_mode">Dark mode: </label> + <label for="locale"><%= translate(locale, "Language: ") %></label> + <select name="locale" id="locale"> + <% LOCALES.each_key do |option| %> + <option value="<%= option %>" <% if user.preferences.locale == option %> selected <% end %>><%= option %></option> + <% end %> + </select> + </div> + + <div class="pure-control-group"> + <label for="dark_mode"><%= translate(locale, "Dark mode: ") %></label> <input name="dark_mode" id="dark_mode" type="checkbox" <% if user.preferences.dark_mode %>checked<% end %>> </div> <div class="pure-control-group"> - <label for="thin_mode">Thin mode: </label> + <label for="thin_mode"><%= translate(locale, "Thin mode: ") %></label> <input name="thin_mode" id="thin_mode" type="checkbox" <% if user.preferences.thin_mode %>checked<% end %>> </div> - <legend>Subscription preferences</legend> + <legend><%= translate(locale, "Subscription preferences") %></legend> <div class="pure-control-group"> - <label for="redirect_feed">Redirect homepage to feed: </label> + <label for="redirect_feed"><%= translate(locale, "Redirect homepage to feed: ") %></label> <input name="redirect_feed" id="redirect_feed" type="checkbox" <% if user.preferences.redirect_feed %>checked<% end %>> </div> <div class="pure-control-group"> - <label for="max_results">Number of videos shown in feed: </label> + <label for="max_results"><%= translate(locale, "Number of videos shown in feed: ") %></label> <input name="max_results" id="max_results" type="number" value="<%= user.preferences.max_results %>"> </div> <div class="pure-control-group"> - <label for="sort">Sort videos by: </label> + <label for="sort"><%= translate(locale, "Sort videos by: ") %></label> <select name="sort" id="sort"> <% {"published", "published - reverse", "alphabetically", "alphabetically - reverse", "channel name", "channel name - reverse"}.each do |option| %> - <option <% if user.preferences.sort == option %> selected <% end %>><%= option %></option> + <option value="<%= option %>" <% if user.preferences.sort == option %> selected <% end %>><%= translate(locale, option) %></option> <% end %> </select> </div> @@ -143,39 +152,39 @@ function update_value(element) { </div> <div class="pure-control-group"> - <label for="unseen_only">Only show unwatched: </label> + <label for="unseen_only"><%= translate(locale, "Only show unwatched: ") %></label> <input name="unseen_only" id="unseen_only" type="checkbox" <% if user.preferences.unseen_only %>checked<% end %>> </div> <div class="pure-control-group"> - <label for="notifications_only">Only show notifications (if there are any): </label> + <label for="notifications_only"><%= translate(locale, "Only show notifications (if there are any): ") %></label> <input name="notifications_only" id="notifications_only" type="checkbox" <% if user.preferences.notifications_only %>checked<% end %>> </div> - <legend>Data preferences</legend> + <legend><%= translate(locale, "Data preferences") %></legend> <div class="pure-control-group"> - <a href="/clear_watch_history?referer=<%= URI.escape(referer) %>">Clear watch history</a> + <a href="/clear_watch_history?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Clear watch history") %></a> </div> <div class="pure-control-group"> - <a href="/data_control?referer=<%= URI.escape(referer) %>">Import/Export data</a> + <a href="/data_control?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Import/Export data") %></a> </div> <div class="pure-control-group"> - <a href="/subscription_manager">Manage subscriptions</a> + <a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a> </div> <div class="pure-control-group"> - <a href="/feed/history">Watch history</a> + <a href="/feed/history"><%= translate(locale, "Watch history") %></a> </div> <div class="pure-control-group"> - <a href="/delete_account?referer=<%= URI.escape(referer) %>">Delete account</a> + <a href="/delete_account?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Delete account") %></a> </div> <div class="pure-controls"> - <button type="submit" class="pure-button pure-button-primary">Save preferences</button> + <button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Save preferences") %></button> </div> </fieldset> </form> diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index 6885424a..2cfb5b6a 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -13,13 +13,17 @@ <div class="pure-g h-box"> <div class="pure-u-1 pure-u-md-1-5"> <% if page >= 2 %> - <a href="/search?q=<%= HTML.escape(query.not_nil!) %>&page=<%= page - 1 %>">Previous page</a> + <a href="/search?q=<%= HTML.escape(query.not_nil!) %>&page=<%= page - 1 %>"> + <%= translate(locale, "Previous page") %> + </a> <% end %> </div> <div class="pure-u-1 pure-u-md-3-5"></div> <div style="text-align:right;" class="pure-u-1 pure-u-md-1-5"> <% if count >= 20 %> - <a href="/search?q=<%= HTML.escape(query.not_nil!) %>&page=<%= page + 1 %>">Next page</a> + <a href="/search?q=<%= HTML.escape(query.not_nil!) %>&page=<%= page + 1 %>"> + <%= translate(locale, "Next page") %> + </a> <% end %> </div> </div> diff --git a/src/invidious/views/subscription_manager.ecr b/src/invidious/views/subscription_manager.ecr index f61db909..3c0836ea 100644 --- a/src/invidious/views/subscription_manager.ecr +++ b/src/invidious/views/subscription_manager.ecr @@ -1,19 +1,19 @@ <% content_for "header" do %> -<title>Subscription manager - Invidious</title> +<title><%= translate(locale, "Subscription manager") %> - Invidious</title> <% end %> <div class="pure-g h-box"> <div class="pure-u-1-3"> - <h3><span id="count"><%= subscriptions.size %></span> subscriptions</h3> + <h3><%= translate(locale, "`x` subscriptions", %(<span id="count">#{subscriptions.size}</span>)) %></h3> </div> <div class="pure-u-1-3" style="text-align:center;"> <h3> - <a href="/feed/history">Watch history</a> + <a href="/feed/history"><%= translate(locale, "Watch history") %></a> </h3> </div> <div class="pure-u-1-3" style="text-align:right;"> <h3> - <a href="/data_control?referer=<%= referer %>">Import/Export</a> + <a href="/data_control?referer=<%= referer %>"><%= translate(locale, "Import/Export") %></a> </h3> </div> </div> @@ -33,7 +33,7 @@ data-id="<%= channel.id %>" onmouseenter='this["href"]="javascript:void(0)"' href="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>"> - unsubscribe + <%= translate(locale, "unsubscribe") %> </a> </h3> </div> diff --git a/src/invidious/views/subscriptions.ecr b/src/invidious/views/subscriptions.ecr index 1f65bbb8..6679cdf9 100644 --- a/src/invidious/views/subscriptions.ecr +++ b/src/invidious/views/subscriptions.ecr @@ -1,16 +1,16 @@ <% content_for "header" do %> -<title>Subscriptions - Invidious</title> +<title><%= translate(locale, "Subscriptions") %> - Invidious</title> <% end %> <div class="pure-g h-box"> <div class="pure-u-1-3"> <h3> - <a href="/subscription_manager">Manage subscriptions</a> + <a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a> </h3> </div> <div class="pure-u-1-3" style="text-align:center;"> <h3> - <a href="/feed/history">Watch history</a> + <a href="/feed/history"><%= translate(locale, "Watch history") %></a> </h3> </div> <div class="pure-u-1-3" style="text-align:right;"> @@ -20,7 +20,7 @@ </div> </div> -<center><%= notifications.size %> unseen notifications</center> +<center><%= translate(locale, "`x` unseen notifications", "#{notifications.size}") %></center> <% if !notifications.empty? %> <div class="h-box"> @@ -73,13 +73,17 @@ function mark_watched(target) { <div class="pure-g"> <div class="pure-u-1 pure-u-md-1-5"> <% if page >= 2 %> - <a href="/feed/subscriptions?max_results=<%= max_results %>&page=<%= page - 1 %>">Previous page</a> + <a href="/feed/subscriptions?max_results=<%= max_results %>&page=<%= page - 1 %>"> + <%= translate(locale, "Previous page") %> + </a> <% end %> </div> <div class="pure-u-1 pure-u-md-3-5"></div> <div style="text-align:right;" class="pure-u-1 pure-u-md-1-5"> <% if (videos.size + notifications.size) == max_results %> - <a href="/feed/subscriptions?max_results=<%= max_results %>&page=<%= page + 1 %>">Next page</a> + <a href="/feed/subscriptions?max_results=<%= max_results %>&page=<%= page + 1 %>"> + <%= translate(locale, "Next page") %> + </a> <% end %> </div> </div> diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 0311f07f..8f2d9c9b 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -25,6 +25,8 @@ <% end %> </head> +<% locale = LOCALES[env.get("locale").as(String)]? %> + <body> <div class="pure-g"> <div class="pure-u-1 pure-u-md-2-24"></div> @@ -68,32 +70,46 @@ </a> </div> <div class="pure-u-1-4"> - <a href="/signout?referer=<%= env.get?("current_page") %>&token=<%= env.get?("token") %>&challenge=<%= env.get?("challenge") %>" class="pure-menu-heading">Sign out</a> + <a href="/signout?referer=<%= env.get?("current_page") %>&token=<%= env.get?("token") %>&challenge=<%= env.get?("challenge") %>" class="pure-menu-heading"> + <%= translate(locale, "Sign out") %> + </a> </div> <% else %> - <a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">Login</a> + <a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> + <%= translate(locale, "Login") %> + </a> <% end %> </div> </div> <%= content %> <div class="footer"> - Released under the AGPLv3 by <a href="https://github.com/omarroth">Omar - Roth</a>. - Source available <a - href="https://github.com/omarroth/invidious">here</a>. - <p>Liberapay: + <p> + <a href="https://github.com/omarroth"> + <%= translate(locale, "Released under the AGPLv3 by Omar Roth.") %> + </a> + </p> + <p> + <a href="https://github.com/omarroth/invidious"> + <%= translate(locale, "Source available here.") %> + </a> + </p> + <p><%= translate(locale, "Liberapay: ") %> <a href="https://liberapay.com/omarroth"> https://liberapay.com/omarroth </a> </p> - <p>Patreon: + <p><%= translate(locale, "Patreon: ") %> <a href="https://patreon.com/omarroth"> https://patreon.com/omarroth </a> </p> - <p>BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</p> - <p>BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</p> - <p>View <a rel="jslicense" href="/licenses">JavaScript license information</a>.</p> + <p><%= translate(locale, "BTC: ") %>356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</p> + <p><%= translate(locale, "BCH: ") %>qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</p> + <p> + <a rel="jslicense" href="/licenses"> + <%= translate(locale, "View JavaScript license information.") %> + </a> + </p> </div> </div> <div class="pure-u-1 pure-u-md-2-24"></div> diff --git a/src/invidious/views/top.ecr b/src/invidious/views/top.ecr index 4dfc3b64..c120e57c 100644 --- a/src/invidious/views/top.ecr +++ b/src/invidious/views/top.ecr @@ -1,3 +1,7 @@ +<% content_for "header" do %> +<title><%= translate(locale, "Top") %> - Invidious</title> +<% end %> + <div class="pure-g"> <% top_videos.each_slice(4) do |slice| %> <% slice.each do |item| %> diff --git a/src/invidious/views/trending.ecr b/src/invidious/views/trending.ecr index b3de793c..d7a082d8 100644 --- a/src/invidious/views/trending.ecr +++ b/src/invidious/views/trending.ecr @@ -1,5 +1,5 @@ <% content_for "header" do %> -<title>Trending - Invidious</title> +<title><%= translate(locale, "Trending") %> - Invidious</title> <% end %> <div class="pure-g"> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 27a6ba15..ad25e5d7 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -52,11 +52,11 @@ <div class="pure-g"> <div class="pure-u-1 pure-u-md-1-5"> <div class="h-box"> - <p><a href="https://www.youtube.com/watch?v=<%= video.id %>">Watch video on YouTube</a></p> + <p><a href="https://www.youtube.com/watch?v=<%= video.id %>"><%= translate(locale, "Watch video on Youtube") %></a></p> <p><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p> <p><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p> <p><i class="icon ion-ios-thumbs-down"></i> <%= number_with_separator(video.dislikes) %></p> - <p id="Genre">Genre: + <p id="Genre"><%= translate(locale, "Genre: ") %> <% if video.genre_url.empty? %> <%= video.genre %> <% else %> @@ -64,18 +64,18 @@ <% end %> </p> <% if !video.license.empty? %> - <p id="License">License: <%= video.license %></p> + <p id="License"><%= translate(locale, "License: ") %><%= video.license %></p> <% end %> - <p id="FamilyFriendly">Family friendly? <%= video.is_family_friendly %></p> - <p id="Wilson">Wilson score: <%= video.wilson_score.round(4) %></p> - <p id="Rating">Rating: <%= rating.round(4) %> / 5</p> - <p id="Engagement">Engagement: <%= engagement.round(2) %>%</p> + <p id="FamilyFriendly"><%= translate(locale, "Family friendly? ") %><%= video.is_family_friendly %></p> + <p id="Wilson"><%= translate(locale, "Wilson score: ") %><%= video.wilson_score.round(4) %></p> + <p id="Rating"><%= translate(locale, "Rating: ") %><%= rating.round(4) %> / 5</p> + <p id="Engagement"><%= translate(locale, "Engagement: ") %><%= engagement.round(2) %>%</p> <% if video.allowed_regions.size != REGIONS.size %> <p id="AllowedRegions"> <% if video.allowed_regions.size < REGIONS.size / 2 %> - Whitelisted regions: <%= video.allowed_regions.join(", ") %> + <%= translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %> <% else %> - Blacklisted regions: <%= (REGIONS.to_a - video.allowed_regions).join(", ") %> + <%= translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %> <% end %> </p> <% end %> @@ -94,14 +94,14 @@ <p> <a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary" href="/subscription_ajax?action_remove_subscriptions=1&c=<%= video.ucid %>&referer=<%= env.get("current_page") %>"> - <b>Unsubscribe | <%= video.sub_count_text %></b> + <b><%= translate(locale, "Unsubscribe") %> | <%= video.sub_count_text %></b> </a> </p> <% else %> <p> <a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary" href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= video.ucid %>&referer=<%= env.get("current_page") %>"> - <b>Subscribe | <%= video.sub_count_text %></b> + <b><%= translate(locale, "Subscribe") %> | <%= video.sub_count_text %></b> </a> </p> <% end %> @@ -109,12 +109,12 @@ <p> <a id="subscribe" class="pure-button pure-button-primary" href="/login?referer=<%= env.get("current_page") %>"> - <b>Login to subscribe to <%= video.author %></b> + <b><%= translate(locale, "Login to subscribe to `x`", video.author) %></b> </a> </p> <% end %> <p> - <b>Shared <%= video.published.to_s("%B %-d, %Y") %></b> + <b><%= translate(locale, "Shared `x`", video.published.to_s("%B %-d, %Y")) %></b> </p> <div> <%= video.description %> @@ -125,8 +125,9 @@ <%= comment_html %> <% else %> <noscript> - Hi! Looks like you have JavaScript disabled. Click <a href="/watch?<%= env.params.query %>&nojs=1">here</a> to view - comments, keep in mind it may take a bit longer to load. + <a href="/watch?<%= env.params.query %>&nojs=1"> + <%= translate(locale, "Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.") %> + </a> </noscript> <% end %> </div> @@ -145,7 +146,7 @@ <% if !rvs.empty? %> <div id="continue" <% if plid %>style="display:none"<% end %>> <div class="pure-control-group"> - <label for="continue">Autoplay next video: </label> + <label for="continue"><%= translate(locale, "Autoplay next video: ") %></label> <input name="continue" onclick="continue_autoplay(this)" id="continue" type="checkbox" <% if params[:continue] %>checked<% end %>> </div> <hr> @@ -241,7 +242,7 @@ function subscribe() { if (xhr.status == 200) { subscribe_button = document.getElementById("subscribe"); subscribe_button.onclick = unsubscribe; - subscribe_button.innerHTML = '<b>Unsubscribe | <%= video.sub_count_text %></b>' + subscribe_button.innerHTML = '<b><%= translate(locale, "Unsubscribe") %> | <%= video.sub_count_text %></b>' } } } @@ -260,7 +261,7 @@ function unsubscribe() { if (xhr.status == 200) { subscribe_button = document.getElementById("subscribe"); subscribe_button.onclick = subscribe; - subscribe_button.innerHTML = '<b>Subscribe | <%= video.sub_count_text %></b>' + subscribe_button.innerHTML = '<b><%= translate(locale, "Subscribe") %> | <%= video.sub_count_text %></b>' } } } @@ -276,9 +277,9 @@ function get_playlist() { var plid = "<%= plid %>" if (plid.startsWith("RD")) { - var plid_url = "/api/v1/mixes/<%= plid %>?continuation=<%= video.id %>&format=html"; + var plid_url = "/api/v1/mixes/<%= plid %>?continuation=<%= video.id %>&format=html&hl=<%= env.get("locale").as(String) %>"; } else { - var plid_url = "/api/v1/playlists/<%= plid %>?continuation=<%= video.id %>&format=html"; + var plid_url = "/api/v1/playlists/<%= plid %>?continuation=<%= video.id %>&format=html&hl=<%= env.get("locale").as(String) %>"; } var xhr = new XMLHttpRequest(); @@ -335,7 +336,7 @@ function get_reddit_comments() { comments.innerHTML = '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>'; - var url = "/api/v1/comments/<%= video.id %>?source=reddit&format=html"; + var url = "/api/v1/comments/<%= video.id %>?source=reddit&format=html&hl=<%= env.get("locale").as(String) %>"; var xhr = new XMLHttpRequest(); xhr.responseType = "json"; xhr.timeout = 20000; @@ -354,12 +355,12 @@ function get_reddit_comments() { <p> \ <b> \ <a href="javascript:void(0)" onclick="swap_comments(\'youtube\')"> \ - View YouTube comments \ + <%= translate(locale, "View YouTube comments") %> \ </a> \ </b> \ </p> \ <b> \ - <a rel="noopener" target="_blank" href="https://reddit.com{permalink}">View more comments on Reddit</a> \ + <a rel="noopener" target="_blank" href="https://reddit.com{permalink}"><%= translate(locale, "View more comments on Reddit") %></a> \ </b> \ </div> \ <div>{contentHtml}</div> \ @@ -391,7 +392,7 @@ function get_youtube_comments() { comments.innerHTML = '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>'; - var url = "/api/v1/comments/<%= video.id %>?format=html"; + var url = "/api/v1/comments/<%= video.id %>?format=html&hl=<%= env.get("locale").as(String) %>"; var xhr = new XMLHttpRequest(); xhr.responseType = "json"; xhr.timeout = 20000; @@ -406,11 +407,11 @@ function get_youtube_comments() { <div> \ <h3> \ <a href="javascript:void(0)" onclick="toggle_comments(this)">[ - ]</a> \ - View {commentCount} comments \ + <%= translate(locale, "View `x` comments", "{commentCount}") %> \ </h3> \ <b> \ <a href="javascript:void(0)" onclick="swap_comments(\'reddit\')"> \ - View Reddit comments \ + <%= translate(locale, "View Reddit comments") %> \ </a> \ </b> \ </div> \ @@ -449,7 +450,7 @@ function get_youtube_replies(target, load_more) { body.innerHTML = '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>'; - var url = '/api/v1/comments/<%= video.id %>?format=html&continuation=' + + var url = '/api/v1/comments/<%= video.id %>?format=html&hl=<%= env.get("locale").as(String) %>&continuation=' + continuation; var xhr = new XMLHttpRequest(); xhr.responseType = 'json'; @@ -467,7 +468,7 @@ function get_youtube_replies(target, load_more) { } else { body.innerHTML = ' \ <p><a href="javascript:void(0)" \ - onclick="hide_youtube_replies(this)">Hide replies \ + onclick="hide_youtube_replies(this)"><%= translate(locale, "Hide replies") %> \ </a></p> \ <div>{contentHtml}</div>'.supplant({ contentHtml: xhr.response.contentHtml, |
