1
0
mirror of https://github.com/webrecorder/pywb.git synced 2025-03-15 00:03:28 +01:00

Localization and Banner Update (#517)

* banner: add banner and localization improvements from ukwa branch:
- show 'view all captures' link if not live
- optional logo
- loc options, if available
- banner options set via window.banner_info in banner.html

localization support: 
- add init_loc() to templateview
- loc available if config options set
- tests: add tests for loading localized messages, override .gitignore to allow test messages.mo
This commit is contained in:
Ilya Kreymer 2019-11-11 09:51:26 -08:00 committed by GitHub
parent 66ac3ca114
commit 0d819aadeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 457 additions and 37 deletions

View File

@ -1,6 +1,6 @@
from gevent.monkey import patch_all; patch_all() 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 werkzeug.wsgi import pop_path_info
from six.moves.urllib.parse import urljoin from six.moves.urllib.parse import urljoin
from six import iteritems from six import iteritems
@ -138,6 +138,17 @@ class FrontEndApp(object):
:rtype: None :rtype: None
""" """
routes = self._make_coll_routes(coll_prefix) 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 = '/<any({0}):lang>'.format(submount_route)
self.url_map.add(Submount(submount_route, routes))
for route in routes: for route in routes:
self.url_map.add(route) self.url_map.add(route)

View File

@ -68,6 +68,11 @@ class RewriterApp(object):
jinja_env.jinja_env.install_null_translations() jinja_env.jinja_env.install_null_translations()
self.jinja_env = jinja_env 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') self.redirect_to_exact = config.get('redirect_to_exact')

View File

@ -3,11 +3,13 @@ from warcio.timeutils import timestamp_now
from pywb.utils.loaders import load 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 jinja2 import FileSystemLoader, PackageLoader, ChoiceLoader
from babel.support import Translations
from webassets.ext.jinja2 import AssetsExtension from webassets.ext.jinja2 import AssetsExtension
from webassets.loaders import YAMLLoader from webassets.loaders import YAMLLoader
from webassets.env import Resolver from webassets.env import Resolver
@ -115,6 +117,90 @@ class JinjaEnv(object):
return loaders 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): def template_filter(self, param=None):
"""Returns a decorator that adds the wrapped function to dictionary of template filters. """Returns a decorator that adds the wrapped function to dictionary of template filters.

9
pywb/static/calendar.svg Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 8 8" style="enable-background:new 0 0 8 8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<path class="st0" d="M0,0v2h7V0H0z M0,3v4.9C0,8,0,8,0.1,8h6.8C7,8,7,8,7,7.9V3H0L0,3z M1,4h1v1H1V4z M3,4h1v1H3V4z M5,4h1v1H5V4z
M1,6h1v1H1V6z M3,6h1v1H3V6z"/>
</svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@ -1,5 +1,5 @@
#_wb_plain_banner, #_wb_frame_top_banner #_wb_frame_top_banner
{ {
display: block !important; display: block !important;
top: 0px !important; top: 0px !important;
@ -9,24 +9,17 @@
font-size: 18px !important; font-size: 18px !important;
background-color: #444 !important; background-color: #444 !important;
color: white !important; color: white !important;
text-align: center !important;
z-index: 2147483643 !important; z-index: 2147483643 !important;
line-height: normal !important; line-height: normal !important;
} }
#title_or_url { #title_or_url
{
display: block !important;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding-right: 4px; max-width: 100%;
display: block;
}
#_wb_plain_banner
{
position: absolute !important;
padding: 4px !important;
border: 1px solid !important;
} }
#_wb_frame_top_banner #_wb_frame_top_banner
@ -34,6 +27,111 @@
position: absolute !important; position: absolute !important;
border: 0px; border: 0px;
height: 44px !important; 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 #wb_iframe_div
@ -41,7 +139,7 @@
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 44px 4px 4px 0px; padding: 44px 0px 0px 0px;
border: none; border: none;
box-sizing: border-box; box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
@ -53,7 +151,39 @@
{ {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 2px solid #545454; border: 2px solid #FFF;
border-width: 2px 0 0 0;
padding: 0px 0px 0px 0px; padding: 0px 0px 0px 0px;
overflow: scroll; 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;
}
}

View File

@ -35,7 +35,7 @@ This file is part of pywb, https://github.com/webrecorder/pywb
this.last_state = {}; this.last_state = {};
this.state = null; this.state = null;
this.title = ''; this.title = '';
this.loadingId = 'bannerLoading'; this.bannerUrlSet = false;
this.onMessage = this.onMessage.bind(this); this.onMessage = this.onMessage.bind(this);
} }
@ -64,7 +64,7 @@ This file is part of pywb, https://github.com/webrecorder/pywb
* @returns {boolean} * @returns {boolean}
*/ */
DefaultBanner.prototype.stillIndicatesLoading = function() { 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 // 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 * @desc Creates the underlying HTML elements comprising the banner
* @param {string} bid - The id for 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 = document.createElement('wb_div', true);
this.banner.setAttribute('id', bid); this.banner.setAttribute('id', bid);
this.banner.setAttribute('lang', 'en'); this.banner.setAttribute('lang', 'en');
this.captureInfo = document.createElement('span');
this.captureInfo.innerHTML = if (window.banner_info.logoImg) {
'<span id="' + this.loadingId + '">Loading...</span>'; var logo = document.createElement("a");
this.captureInfo.id = '_wb_capture_info'; logo.setAttribute("href", "/" + (window.banner_info.locale ? window.banner_info.locale + "/" : ""));
logo.setAttribute("class", "_wb_linked_logo");
var logoContents = "";
logoContents += "<img src='" + window.banner_info.logoImg + "' alt='" + window.banner_info.logoAlt + "'>";
logoContents += "<img src='" + window.banner_info.logoImg + "' class='mobile' alt='" + 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); 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 = "<img src='" + calendarImg + "' alt='" + window.banner_info.calendarAlt + "'><span class='no-mobile'>&nbsp;" +window.banner_info.calendarLabel + "</span>";
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); 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) { if (is_gmt) {
return date.toGMTString(); return date.toGMTString();
} else { } 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 capture_str;
var title_str; var title_str;
if (!ts) { if (!url) {
this.captureInfo.innerHTML = window.banner_info.loadingLabel;
this.bannerUrlSet = false;
return; return;
} }
var date_str = this.ts_to_date(ts, true); if (!ts) {
return;
}
if (title) { if (title) {
capture_str = title; capture_str = title;
@ -209,20 +281,27 @@ This file is part of pywb, https://github.com/webrecorder/pywb
} }
title_str = capture_str; title_str = capture_str;
capture_str = "<b id='title_or_url'>" + capture_str + '</b>';
capture_str = "<b id='title_or_url' title='" + capture_str + "'>" + capture_str + "</b>";
capture_str += "<span class='_wb_capture_date'>";
if (is_live) { if (is_live) {
title_str = ' pywb Live: ' + title_str; title_str = window.banner_info.liveMsg + " " + title_str;
capture_str += '<i>Live on&nbsp;</i>'; capture_str += "<b>" + window.banner_info.liveMsg + "&nbsp;</b>";
} else {
title_str += 'pywb Archived: ' + title_str;
capture_str += '<i>Archived on&nbsp;</i>';
} }
title_str += ' (' + date_str + ')'; capture_str += this.ts_to_date(ts, window.banner_info.is_gmt);
capture_str += date_str; capture_str += "</span>";
this.calendarLink.setAttribute("href", window.banner_info.prefix + "*/" + url);
this.calendarLink.style.display = is_live ? "none" : "";
this.captureInfo.innerHTML = capture_str; this.captureInfo.innerHTML = capture_str;
window.document.title = title_str; window.document.title = title_str;
this.bannerUrlSet = true;
}; };
// all banners will expose themselves by adding themselves as WBBanner on window // all banners will expose themselves by adding themselves as WBBanner on window

View File

@ -2,4 +2,26 @@
<!-- default banner, create through js --> <!-- default banner, create through js -->
<script src='{{ static_prefix }}/default_banner.js'> </script> <script src='{{ static_prefix }}/default_banner.js'> </script>
<link rel='stylesheet' href='{{ static_prefix }}/default_banner.css'/> <link rel='stylesheet' href='{{ static_prefix }}/default_banner.css'/>
<script>
window.banner_info = {
is_gmt: true,
liveMsg: decodeURIComponent("{{ _Q('Live on') }}"),
calendarAlt: decodeURIComponent("{{ _Q('Calendar icon') }}"),
calendarLabel: decodeURIComponent("{{ _Q('View All Captures') }}"),
choiceLabel: decodeURIComponent("{{ _Q('Language:') }}"),
loadingLabel: decodeURIComponent("{{ _Q('Loading...') }}"),
logoAlt: decodeURIComponent("{{ _Q('Logo') }}"),
locale: "{{ env.pywb_lang | default('en') }}",
curr_locale: "{{ env.pywb_lang }}",
locales: {{ locales }},
locale_prefixes: {{ get_locale_prefixes() | tojson }},
prefix: "{{ wb_prefix }}",
staticPrefix: "{{ static_prefix }}"
};
</script>
{% endif %} {% endif %}

View File

@ -2,7 +2,7 @@
{% block body %} {% block body %}
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<h2 class="display-2">Pywb Wayback Machine</h2> <h2 class="display-2">{{ _('Pywb Wayback Machine') }}</h2>
<p class="lead">This archive contains the following collections:</p> <p class="lead">This archive contains the following collections:</p>
</div> </div>
<div class="row"> <div class="row">

View File

@ -14,3 +14,4 @@ portalocker
wsgiprox>=1.5.1 wsgiprox>=1.5.1
fakeredis<1.0 fakeredis<1.0
tldextract tldextract
babel

View File

@ -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'

1
tests/i18n-data/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
#allow .mo

Binary file not shown.

View File

@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language: lt\n"
"Language-Team: lt <LL@li.org>\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"

31
tests/test_locales.py Normal file
View File

@ -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