diff --git a/pywb/apps/frontendapp.py b/pywb/apps/frontendapp.py index e6211d16..00ceb7d3 100644 --- a/pywb/apps/frontendapp.py +++ b/pywb/apps/frontendapp.py @@ -1,6 +1,6 @@ from gevent.monkey import patch_all; patch_all() -from werkzeug.routing import Map, Rule, RequestRedirect +from werkzeug.routing import Map, Rule, RequestRedirect, Submount from werkzeug.wsgi import pop_path_info from six.moves.urllib.parse import urljoin from six import iteritems @@ -138,6 +138,17 @@ class FrontEndApp(object): :rtype: None """ routes = self._make_coll_routes(coll_prefix) + + # init loc routes, if any + loc_keys = list(self.rewriterapp.loc_map.keys()) + if loc_keys: + routes.append(Rule('/', endpoint=self.serve_home)) + + submount_route = ', '.join(loc_keys) + submount_route = '/'.format(submount_route) + + self.url_map.add(Submount(submount_route, routes)) + for route in routes: self.url_map.add(route) diff --git a/pywb/apps/rewriterapp.py b/pywb/apps/rewriterapp.py index 98a7f953..155e4f71 100644 --- a/pywb/apps/rewriterapp.py +++ b/pywb/apps/rewriterapp.py @@ -68,6 +68,11 @@ class RewriterApp(object): jinja_env.jinja_env.install_null_translations() self.jinja_env = jinja_env + self.loc_map = {} + + self.jinja_env.init_loc(self.config.get('locales_root_dir'), + self.config.get('locales'), + self.loc_map) self.redirect_to_exact = config.get('redirect_to_exact') diff --git a/pywb/rewrite/templateview.py b/pywb/rewrite/templateview.py index 6dd83094..1c9f194a 100644 --- a/pywb/rewrite/templateview.py +++ b/pywb/rewrite/templateview.py @@ -3,11 +3,13 @@ from warcio.timeutils import timestamp_now from pywb.utils.loaders import load -from six.moves.urllib.parse import urlsplit +from six.moves.urllib.parse import urlsplit, quote -from jinja2 import Environment, TemplateNotFound +from jinja2 import Environment, TemplateNotFound, contextfunction from jinja2 import FileSystemLoader, PackageLoader, ChoiceLoader +from babel.support import Translations + from webassets.ext.jinja2 import AssetsExtension from webassets.loaders import YAMLLoader from webassets.env import Resolver @@ -115,6 +117,90 @@ class JinjaEnv(object): return loaders + def init_loc(self, locales_root_dir, locales, loc_map): + locales = locales or [] + + if locales_root_dir: + for loc in locales: + loc_map[loc] = Translations.load(locales_root_dir, [loc, 'en']) + #jinja_env.jinja_env.install_gettext_translations(translations) + + def get_translate(context): + loc = context.get('env', {}).get('pywb_lang') + return loc_map.get(loc) + + def override_func(jinja_env, name): + @contextfunction + def get_override(context, text): + translate = get_translate(context) + if not translate: + return text + + func = getattr(translate, name) + return func(text) + + jinja_env.globals[name] = get_override + + # standard gettext() translation function + override_func(self.jinja_env, 'gettext') + + # single/plural form translation function + override_func(self.jinja_env, 'ngettext') + + # Special _Q() function to return %-encoded text, necessary for use + # with text in banner + @contextfunction + def quote_gettext(context, text): + translate = get_translate(context) + if not translate: + return text + + text = translate.gettext(text) + return quote(text, safe='/: ') + + self.jinja_env.globals['locales'] = list(loc_map.keys()) + self.jinja_env.globals['_Q'] = quote_gettext + + @contextfunction + def switch_locale(context, locale): + environ = context.get('env') + curr_loc = environ.get('pywb_lang', '') + + request_uri = environ.get('REQUEST_URI', environ.get('PATH_INFO')) + + if curr_loc: + return request_uri.replace(curr_loc, locale, 1) + + app_prefix = environ.get('pywb.app_prefix', '') + + if app_prefix and request_uri.startswith(app_prefix): + request_uri = request_uri.replace(app_prefix, '') + + return app_prefix + '/' + locale + request_uri + + @contextfunction + def get_locale_prefixes(context): + environ = context.get('env') + locale_prefixes = {} + + orig_prefix = environ.get('pywb.app_prefix', '') + coll = environ.get('SCRIPT_NAME', '') + + if orig_prefix: + coll = coll[len(orig_prefix):] + + curr_loc = environ.get('pywb_lang', '') + if curr_loc: + coll = coll[len(curr_loc) + 1:] + + for locale in loc_map.keys(): + locale_prefixes[locale] = orig_prefix + '/' + locale + coll + '/' + + return locale_prefixes + + self.jinja_env.globals['switch_locale'] = switch_locale + self.jinja_env.globals['get_locale_prefixes'] = get_locale_prefixes + def template_filter(self, param=None): """Returns a decorator that adds the wrapped function to dictionary of template filters. diff --git a/pywb/static/calendar.svg b/pywb/static/calendar.svg new file mode 100644 index 00000000..a54ae0dc --- /dev/null +++ b/pywb/static/calendar.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/pywb/static/default_banner.css b/pywb/static/default_banner.css index bbed5a2a..3a9565f1 100644 --- a/pywb/static/default_banner.css +++ b/pywb/static/default_banner.css @@ -1,5 +1,5 @@ -#_wb_plain_banner, #_wb_frame_top_banner +#_wb_frame_top_banner { display: block !important; top: 0px !important; @@ -9,24 +9,17 @@ font-size: 18px !important; background-color: #444 !important; color: white !important; - text-align: center !important; z-index: 2147483643 !important; line-height: normal !important; } -#title_or_url { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-right: 4px; - display: block; -} - -#_wb_plain_banner +#title_or_url { - position: absolute !important; - padding: 4px !important; - border: 1px solid !important; + display: block !important; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; } #_wb_frame_top_banner @@ -34,6 +27,111 @@ position: absolute !important; border: 0px; height: 44px !important; + + display: flex !important; + display: -webkit-box !important; + display: -moz-box !important; + display: -webkit-flex !important; + display: -ms-flexbox !important; + + justify-content: space-between; + -webkit-box-pack: justify; + -moz-box-pack: justify; + -ms-flex-pack: justify; + align-items: center; + -webkit-box-align: center; + -moz-box-align: center; + -ms-flex-align: center; +} + +#_wb_frame_top_banner ._wb_linked_logo +{ + display: block; + height: 26px; + width: 71px; + margin-left: 15px; + flex-shrink: 0; + -webkit-flex-shrink: 1 0; + -moz-flex-shrink: 1 0; + -ms-flex: 0 0 71px; +} + +#_wb_frame_top_banner ._wb_linked_logo img +{ + width: auto; + height: 100%; + border: none; +} + +#_wb_capture_info +{ + flex-grow: 1; + -webkit-box-flex: 1; + -moz-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex: 1; + + min-width: 0; + margin: 0 15px; + + display: flex !important; + display: -webkit-box !important; + display: -moz-box !important; + display: -webkit-flex !important; + display: -ms-flexbox !important; + + flex-direction: column; + -webkit-box-direction: normal; + -webkit-box-orient: vertical; + -moz-box-direction: normal; + -moz-box-orient: vertical; + -ms-flex-direction: column; + + justify-content: center; + -webkit-box-pack: center; + -moz-box-pack: center; + -ms-flex-pack: center; + + align-items: center; + -webkit-box-align: center; + -moz-box-align: center; + -ms-flex-align: center; + + height: 100%; + -webkit-font-smoothing: antialiased; +} + +._wb_capture_date +{ + font-size: 13px; +} + +#_wb_frame_top_banner #_wb_ancillary_links +{ + font-size: 12px; + color: #FFF; + margin-right: 15px; + text-align: right; + flex-shrink: 1 0; + -webkit-flex-shrink: 1 0; + -moz-flex-shrink: 1 0; + -ms-flex: 0 0 115px; +} +#_wb_frame_top_banner #_wb_ancillary_links a:link, +#_wb_frame_top_banner #_wb_ancillary_links a:visited, +#_wb_frame_top_banner #_wb_ancillary_links a:active +{ + color: #FFF; + text-decoration: none; +} +#_wb_frame_top_banner #_wb_ancillary_links a:hover +{ + text-decoration: underline; +} +#_wb_frame_top_banner #_wb_ancillary_links a img +{ + width: 10px; + height: 10px; } #wb_iframe_div @@ -41,7 +139,7 @@ position: absolute; width: 100%; height: 100%; - padding: 44px 4px 4px 0px; + padding: 44px 0px 0px 0px; border: none; box-sizing: border-box; -moz-box-sizing: border-box; @@ -53,7 +151,39 @@ { width: 100%; height: 100%; - border: 2px solid #545454; + border: 2px solid #FFF; + border-width: 2px 0 0 0; padding: 0px 0px 0px 0px; overflow: scroll; } + +.mobile { + display: none; +} + +@media screen and (max-width: 500px) { + #_wb_frame_top_banner ._wb_linked_logo + { + width: 26px; + height: 26px; + margin-left: 10px; + } + #_wb_frame_top_banner ._wb_linked_logo img:not(.mobile) + { + display: none; + } + #_wb_frame_top_banner .mobile + { + display: block; + } + + #_wb_capture_info + { + margin: 0 5px; + } + + #_wb_frame_top_banner .no-mobile + { + display: none; + } +} diff --git a/pywb/static/default_banner.js b/pywb/static/default_banner.js index 58b43c2a..5fd71e01 100644 --- a/pywb/static/default_banner.js +++ b/pywb/static/default_banner.js @@ -35,7 +35,7 @@ This file is part of pywb, https://github.com/webrecorder/pywb this.last_state = {}; this.state = null; this.title = ''; - this.loadingId = 'bannerLoading'; + this.bannerUrlSet = false; this.onMessage = this.onMessage.bind(this); } @@ -64,7 +64,7 @@ This file is part of pywb, https://github.com/webrecorder/pywb * @returns {boolean} */ DefaultBanner.prototype.stillIndicatesLoading = function() { - return document.getElementById(this.loadingId) != null; + return !this.bannerUrlSet; }; /** @@ -129,6 +129,21 @@ This file is part of pywb, https://github.com/webrecorder/pywb // Functions internal to the default banner + /** + * @desc Navigate to different language, if available + */ + + DefaultBanner.prototype.changeLanguage = function(lang, evt) { + evt.preventDefault(); + var path = window.location.href; + if (path.indexOf(window.banner_info.prefix) == 0) { + path = path.substring(window.banner_info.prefix.length); + if (window.banner_info.locale_prefixes && window.banner_info.locale_prefixes[lang]) { + window.location.pathname = window.banner_info.locale_prefixes[lang] + path; + } + } + } + /** * @desc Creates the underlying HTML elements comprising the banner * @param {string} bid - The id for the banner @@ -137,11 +152,64 @@ This file is part of pywb, https://github.com/webrecorder/pywb this.banner = document.createElement('wb_div', true); this.banner.setAttribute('id', bid); this.banner.setAttribute('lang', 'en'); - this.captureInfo = document.createElement('span'); - this.captureInfo.innerHTML = - 'Loading...'; - this.captureInfo.id = '_wb_capture_info'; + + if (window.banner_info.logoImg) { + var logo = document.createElement("a"); + logo.setAttribute("href", "/" + (window.banner_info.locale ? window.banner_info.locale + "/" : "")); + logo.setAttribute("class", "_wb_linked_logo"); + + var logoContents = ""; + logoContents += "" + window.banner_info.logoAlt + ""; + logoContents += "" + window.banner_info.logoAlt + ""; + + logo.innerHTML = logoContents; + this.banner.appendChild(logo); + } + + this.captureInfo = document.createElement("span"); + this.captureInfo.setAttribute("id", "_wb_capture_info"); + this.captureInfo.innerHTML = window.banner_info.loadingLabel; this.banner.appendChild(this.captureInfo); + + var ancillaryLinks = document.createElement("div"); + ancillaryLinks.setAttribute("id", "_wb_ancillary_links"); + + var calendarImg = window.banner_info.calendarImg || window.banner_info.staticPrefix + "/calendar.svg"; + + var calendarLink = document.createElement("a"); + calendarLink.setAttribute("id", "calendarLink"); + calendarLink.setAttribute("href", "#"); + calendarLink.innerHTML = "" + window.banner_info.calendarAlt + " " +window.banner_info.calendarLabel + ""; + ancillaryLinks.appendChild(calendarLink); + this.calendarLink = calendarLink; + + if (typeof window.banner_info.locales !== "undefined" && window.banner_info.locales.length) { + var locales = window.banner_info.locales; + var languages = document.createElement("div"); + + var label = document.createElement("span"); + label.setAttribute("class", "no-mobile"); + label.appendChild(document.createTextNode(window.banner_info.choiceLabel + " ")); + languages.appendChild(label); + + for(var i = 0; i < locales.length; i++) { + var locale = locales[i]; + var langLink = document.createElement("a"); + langLink.setAttribute("href", "#"); + langLink.addEventListener("click", this.changeLanguage.bind(this, locale)); + langLink.appendChild(document.createTextNode(locale)); + + languages.appendChild(langLink); + if (i !== locales.length - 1) { + languages.appendChild(document.createTextNode(" / ")); + } + } + + ancillaryLinks.appendChild(languages); + } + + this.banner.appendChild(ancillaryLinks); + document.body.insertBefore(this.banner, document.body.firstChild); }; @@ -181,7 +249,7 @@ This file is part of pywb, https://github.com/webrecorder/pywb if (is_gmt) { return date.toGMTString(); } else { - return date.toLocaleString(); + return date.toLocaleString(window.banner_info.locale); } }; @@ -196,11 +264,15 @@ This file is part of pywb, https://github.com/webrecorder/pywb var capture_str; var title_str; - if (!ts) { + if (!url) { + this.captureInfo.innerHTML = window.banner_info.loadingLabel; + this.bannerUrlSet = false; return; } - var date_str = this.ts_to_date(ts, true); + if (!ts) { + return; + } if (title) { capture_str = title; @@ -209,20 +281,27 @@ This file is part of pywb, https://github.com/webrecorder/pywb } title_str = capture_str; - capture_str = "" + capture_str + ''; + + capture_str = "" + capture_str + ""; + + capture_str += ""; if (is_live) { - title_str = ' pywb Live: ' + title_str; - capture_str += 'Live on '; - } else { - title_str += 'pywb Archived: ' + title_str; - capture_str += 'Archived on '; + title_str = window.banner_info.liveMsg + " " + title_str; + capture_str += "" + window.banner_info.liveMsg + " "; } - title_str += ' (' + date_str + ')'; - capture_str += date_str; + capture_str += this.ts_to_date(ts, window.banner_info.is_gmt); + capture_str += ""; + + this.calendarLink.setAttribute("href", window.banner_info.prefix + "*/" + url); + this.calendarLink.style.display = is_live ? "none" : ""; + this.captureInfo.innerHTML = capture_str; + window.document.title = title_str; + + this.bannerUrlSet = true; }; // all banners will expose themselves by adding themselves as WBBanner on window diff --git a/pywb/templates/banner.html b/pywb/templates/banner.html index 1b409f5e..f458db79 100644 --- a/pywb/templates/banner.html +++ b/pywb/templates/banner.html @@ -2,4 +2,26 @@ + + + {% endif %} diff --git a/pywb/templates/index.html b/pywb/templates/index.html index ecd1c380..9157368b 100644 --- a/pywb/templates/index.html +++ b/pywb/templates/index.html @@ -2,7 +2,7 @@ {% block body %}
-

Pywb Wayback Machine

+

{{ _('Pywb Wayback Machine') }}

This archive contains the following collections:

diff --git a/requirements.txt b/requirements.txt index be83a8f2..d666d6d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ portalocker wsgiprox>=1.5.1 fakeredis<1.0 tldextract +babel diff --git a/tests/config_test_loc.yaml b/tests/config_test_loc.yaml new file mode 100644 index 00000000..95178cca --- /dev/null +++ b/tests/config_test_loc.yaml @@ -0,0 +1,14 @@ +debug: true + +collections: + pywb: + index_paths: ./sample_archive/cdx/ + archive_paths: ./sample_archive/warcs/ + +# i18n +locales_root_dir: ./tests/i18n-data/ +locales: + - en + - 'l337' + + diff --git a/tests/i18n-data/.gitignore b/tests/i18n-data/.gitignore new file mode 100644 index 00000000..4d8f59ea --- /dev/null +++ b/tests/i18n-data/.gitignore @@ -0,0 +1 @@ +#allow .mo diff --git a/tests/i18n-data/l337/LC_MESSAGES/messages.mo b/tests/i18n-data/l337/LC_MESSAGES/messages.mo new file mode 100644 index 00000000..294b72a2 Binary files /dev/null and b/tests/i18n-data/l337/LC_MESSAGES/messages.mo differ diff --git a/tests/i18n-data/l337/LC_MESSAGES/messages.po b/tests/i18n-data/l337/LC_MESSAGES/messages.po new file mode 100644 index 00000000..09a310ac --- /dev/null +++ b/tests/i18n-data/l337/LC_MESSAGES/messages.po @@ -0,0 +1,31 @@ +# Lithuanian translations for pywb. +# Copyright (C) 2019 pywb +# This file is distributed under the same license as the pywb project. +# FIRST AUTHOR , 2019. +# +msgid "" +msgstr "" +"Project-Id-Version: pywb 2.4.0rc0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2019-11-06 20:20-0800\n" +"PO-Revision-Date: 2019-11-06 20:25-0800\n" +"Last-Translator: FULL NAME \n" +"Language: lt\n" +"Language-Team: lt \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"(n%100<10 || n%100>=20) ? 1 : 2)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.5.3\n" + +#: pywb/templates/banner.html:14 +msgid "Language:" +msgstr "L4n9u4g3:" + + +#: pywb/templates/index.html:5 +msgid "Pywb Wayback Machine" +msgstr "Py\\/\\/b W4yb4ck /\\/\\4ch1n3" + + diff --git a/tests/test_locales.py b/tests/test_locales.py new file mode 100644 index 00000000..76773b64 --- /dev/null +++ b/tests/test_locales.py @@ -0,0 +1,31 @@ +from .base_config_test import BaseConfigTest + + +# ============================================================================ +class TestLocales(BaseConfigTest): + @classmethod + def setup_class(cls): + super(TestLocales, cls).setup_class('config_test_loc.yaml') + + def test_locale_en_home(self): + res = self.testapp.get('/en/') + + assert 'Pywb Wayback Machine' in res.text, res.text + + def test_locale_l337_home(self): + res = self.testapp.get('/l337/') + + print(res.text) + assert r'Py\/\/b W4yb4ck /\/\4ch1n3' in res.text + + def test_locale_en_replay_banner(self): + res = self.testapp.get('/en/pywb/mp_/https://example.com/') + assert '"en"' in res.text + assert '"Language:"' in res.text + + def test_locale_l337_replay_banner(self): + res = self.testapp.get('/l337/pywb/mp_/https://example.com/') + assert '"l337"' in res.text + assert '"L4n9u4g3:"' in res.text + +