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

vueui: i18n support (from @vanecat):

- add localized text to frame_insert.html and query.html (for now) to be passed to vueui
- ensure all vue strings localized
- use variable names in localizabls strings, eg. "view capture on {date}"
- simplified text (no cardinal suffixes, use 24-hourt clock)
This commit is contained in:
Ivan Velev 2022-02-08 19:17:12 -08:00 committed by Tessa Walsh
parent 14e464bd1c
commit 72cb588936
12 changed files with 723 additions and 922 deletions

File diff suppressed because one or more lines are too long

View File

@ -18,13 +18,72 @@ html, body
{{ banner_html }}
<script>
var i18nStrings = {
jan_long: "{{ _Q('January') }}",
feb_long: "{{ _Q('February') }}",
mar_long: "{{ _Q('March') }}",
apr_long: "{{ _Q('April') }}",
may_long: "{{ _Q('May') }}",
jun_long: "{{ _Q('June') }}",
jul_long: "{{ _Q('July') }}",
aug_long: "{{ _Q('August') }}",
sep_long: "{{ _Q('September') }}",
oct_long: "{{ _Q('October') }}",
nov_long: "{{ _Q('November') }}",
dec_long: "{{ _Q('December') }}",
jan_short: "{{ _Q('Jan') }}",
feb_short: "{{ _Q('Feb') }}",
mar_short: "{{ _Q('Mar') }}",
apr_short: "{{ _Q('Apr') }}",
may_short: "{{ _Q('May') }}",
jun_short: "{{ _Q('Jun') }}",
jul_short: "{{ _Q('Jul') }}",
aug_short: "{{ _Q('Aug') }}",
sep_short: "{{ _Q('Sep') }}",
oct_short: "{{ _Q('Oct') }}",
nov_short: "{{ _Q('Nov') }}",
dec_short: "{{ _Q('Dec') }}",
mon_short: "{{ _Q('Mon') }}",
tue_short: "{{ _Q('Tue') }}",
wed_short: "{{ _Q('Wed') }}",
thu_short: "{{ _Q('Thu') }}",
fri_short: "{{ _Q('Fri') }}",
sat_short: "{{ _Q('Sat') }}",
sun_short: "{{ _Q('Sun') }}",
mon_long: "{{ _Q('Monday') }}",
tue_long: "{{ _Q('Tuesday') }}",
wed_long: "{{ _Q('Wednesday') }}",
thu_long: "{{ _Q('Thursday') }}",
fri_long: "{{ _Q('Friday') }}",
sat_long: "{{ _Q('Saturday') }}",
sun_long: "{{ _Q('Sunday') }}",
"All-time": "{{ _Q('All-time') }}",
"show timeline":"{{ _Q('show timeline') }}",
"hide timeline":"{{ _Q('hide timeline') }}",
"show calendar":"{{ _Q('show calendar') }}",
"hide calendar":"{{ _Q('hide calendar') }}",
"View capture on {date}":"{{ _Q('View capture on {date}') }}",
"{count} capture":"{{ _Q('{count} capture') }}",
"{count} captures":"{{ _Q('{count} captures') }}",
"{capture_text} on {date}":"{{ _Q('{capture_text} on {date}') }}",
"{capture_text} in {month}":"{{ _Q('{capture_text} in {month}') }}",
"current":"{{ _Q('current') }}",
"Loading...": "{{ _Q('Loading...') }}",
"Current Capture": "{{ _Q('Current Capture') }}",
"capture": "{{ _Q('capture') }}",
"captures": "{{ _Q('captures') }}",
"from {hour1} to {hour2}": "{{ _Q('from {hour1} to {hour2}') }}",
};
</script>
</head>
<body style="margin: 0px; padding: 0px;">
{% if ui.vue_timeline_banner %}
<div id="app" style="width: 100%; height: 200px"></div>
<script>
VueUI.main("{{ static_prefix }}", "{{ url }}", "{{ wb_prefix }}", "{{ timestamp }}", "{{ ui.logo }}");
VueUI.main("{{ static_prefix }}", "{{ url }}", "{{ wb_prefix }}", "{{ timestamp }}", "{{ ui.logo }}", "{{ env.pywb_lang | default('en') }}", i18nStrings);
</script>
{% endif %}

View File

@ -18,6 +18,64 @@
{% endif %}
<script>
var i18nStrings = {
jan_long: "{{ _Q('January') }}",
feb_long: "{{ _Q('February') }}",
mar_long: "{{ _Q('March') }}",
apr_long: "{{ _Q('April') }}",
may_long: "{{ _Q('May') }}",
jun_long: "{{ _Q('June') }}",
jul_long: "{{ _Q('July') }}",
aug_long: "{{ _Q('August') }}",
sep_long: "{{ _Q('September') }}",
oct_long: "{{ _Q('October') }}",
nov_long: "{{ _Q('November') }}",
dec_long: "{{ _Q('December') }}",
jan_short: "{{ _Q('Jan') }}",
feb_short: "{{ _Q('Feb') }}",
mar_short: "{{ _Q('Mar') }}",
apr_short: "{{ _Q('Apr') }}",
may_short: "{{ _Q('May') }}",
jun_short: "{{ _Q('Jun') }}",
jul_short: "{{ _Q('Jul') }}",
aug_short: "{{ _Q('Aug') }}",
sep_short: "{{ _Q('Sep') }}",
oct_short: "{{ _Q('Oct') }}",
nov_short: "{{ _Q('Nov') }}",
dec_short: "{{ _Q('Dec') }}",
mon_short: "{{ _Q('Mon') }}",
tue_short: "{{ _Q('Tue') }}",
wed_short: "{{ _Q('Wed') }}",
thu_short: "{{ _Q('Thu') }}",
fri_short: "{{ _Q('Fri') }}",
sat_short: "{{ _Q('Sat') }}",
sun_short: "{{ _Q('Sun') }}",
mon_long: "{{ _Q('Monday') }}",
tue_long: "{{ _Q('Tuesday') }}",
wed_long: "{{ _Q('Wednesday') }}",
thu_long: "{{ _Q('Thursday') }}",
fri_long: "{{ _Q('Friday') }}",
sat_long: "{{ _Q('Saturday') }}",
sun_long: "{{ _Q('Sunday') }}",
"All-time": "{{ _Q('All-time') }}",
"show timeline":"{{ _Q('show timeline') }}",
"hide timeline":"{{ _Q('hide timeline') }}",
"show calendar":"{{ _Q('show calendar') }}",
"hide calendar":"{{ _Q('hide calendar') }}",
"View capture on {date}":"{{ _Q('View capture on {date}') }}",
"{count} capture":"{{ _Q('{count} capture') }}",
"{count} captures":"{{ _Q('{count} captures') }}",
"{capture_text} on {date}":"{{ _Q('{capture_text} on {date}') }}",
"{capture_text} in {month}":"{{ _Q('{capture_text} in {month}') }}",
"current":"{{ _Q('current') }}", // translators: current capture in list of captures
"Loading...": "{{ _Q('Loading...') }}",
"Current Capture": "{{ _Q('Current Capture') }}",
"capture": "{{ _Q('capture') }}",
"captures": "{{ _Q('captures') }}",
"from {hour1} to {hour2}": "{{ _Q('from {hour1} to {hour2}') }}",
};
</script>
{% endblock %}
@ -80,7 +138,7 @@
renderCal.init();
{% else %}
VueUI.main("{{ static_prefix }}", "{{ url }}", "{{ prefix }}", undefined, "{{ ui.logo }}");
VueUI.main("{{ static_prefix }}", "{{ url }}", "{{ prefix }}", undefined, "{{ ui.logo }}", "{{ env.pywb_lang | default('en') }}", i18nStrings);
{% endif %}

View File

@ -15,10 +15,10 @@
</div>
<div class="toggles">
<span class="toggle" :class="{expanded: showFullView}" @click="showFullView = !showFullView" :title="(showTimelineView ? 'show':'hide') + ' calendar'">
<span class="toggle" :class="{expanded: showFullView}" @click="showFullView = !showFullView" :title="(showTimelineView ? _('show calendar'):_('hide calendar'))">
<img src="/static/calendar-icon.png" />
</span>
<span class="toggle" :class="{expanded: showTimelineView}" @click="showTimelineView = !showTimelineView" :title="(showTimelineView ? 'show':'hide') + ' timeline'">
<span class="toggle" :class="{expanded: showTimelineView}" @click="showTimelineView = !showTimelineView" :title="(showTimelineView ? _('show timeline'):_('hide timeline'))">
<img src="/static/timeline-icon.png" />
</span>
</div>
@ -40,7 +40,7 @@
</form>
<div v-if="currentSnapshot && !showFullView">
<span v-if="config.title">{{ config.title }}</span>
Current capture: {{currentSnapshot.getTimeDateFormatted()}}
{{_('Current Capture')}}: {{currentSnapshot.getTimeDateFormatted()}}
</div>
</div>
<CalendarYear v-if="showFullView && currentPeriod && currentPeriod.children.length"
@ -57,6 +57,7 @@ import TimelineBreadcrumbs from "./components/TimelineBreadcrumbs.vue";
import CalendarYear from "./components/CalendarYear.vue";
import { PywbSnapshot, PywbPeriod } from "./model.js";
import {PywbI18N} from "./i18n";
export default {
name: "PywbReplayApp",
@ -74,7 +75,7 @@ export default {
title: "",
initialView: {}
},
timelineHighlight: false
timelineHighlight: false,
};
},
components: {Timeline, TimelineBreadcrumbs, CalendarYear},
@ -87,6 +88,9 @@ export default {
}
},
methods: {
_(id, embeddedVariableStrings=null) {
return PywbI18N.instance.getText(id, embeddedVariableStrings);
},
gotoPeriod: function(newPeriod, onlyZoomToPeriod) {
if (this.timelineHighlight) {
setTimeout((() => {

View File

@ -91,17 +91,17 @@
<template>
<div class="calendar-month" :class="{current: isCurrent, 'contains-current-snapshot': containsCurrentSnapshot}">
<h3>{{month.getReadableId()}} <span v-if="month.snapshotCount">({{ month.snapshotCount }})</span></h3>
<h3>{{getLongMonthName(month.id)}} <span v-if="month.snapshotCount">({{ month.snapshotCount }})</span></h3>
<div v-if="month.snapshotCount">
<span v-for="(day) in ['S', 'M', 'T', 'W', 'H', 'F', 'S']" class="day" :style="dayStyle">{{day}}</span><br/>
<span v-for="(day,i) in days"><br v-if="i && i % 7===0"/><span class="day" :class="{empty: !day || !day.snapshotCount, 'contains-current-snapshot':dayContainsCurrentSnapshot(day)}" :style="dayStyle" @click="gotoDay(day, $event)"><template v-if="day"><span class="size" v-if="day.snapshotCount" :style="getDayCountCircleStyle(day.snapshotCount)"> </span><span class="day-id">{{day.id}}</span><span v-if="day.snapshotCount" class="count">{{day.snapshotCount}} capture<span v-if="day.snapshotCount!==1">s</span></span></template><template v-else v-html="'&nbsp;'"></template></span></span>
<span v-for="(dayInitial) in dayInitials" class="day" :style="dayStyle">{{dayInitial}}</span><br/>
<span v-for="(day,i) in days"><br v-if="i && i % 7===0"/><span class="day" :class="{empty: !day || !day.snapshotCount, 'contains-current-snapshot':dayContainsCurrentSnapshot(day)}" :style="dayStyle" @click="gotoDay(day, $event)"><template v-if="day"><span class="size" v-if="day.snapshotCount" :style="getDayCountCircleStyle(day.snapshotCount)"> </span><span class="day-id">{{day.id}}</span><span v-if="day.snapshotCount" class="count">{{ $root._(day.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: day.snapshotCount}) }}</span></template><template v-else v-html="'&nbsp;'"></template></span></span>
</div>
<div v-else class="empty">no captures</div>
</div>
</template>
<script>
import {PywbPeriod} from "../model";
import {PywbI18N} from "../i18n";
export default {
props: ["month", "year", "isCurrent", "yearContainsCurrentSnapshot", "currentSnapshot"],
@ -112,6 +112,9 @@ export default {
};
},
computed: {
dayInitials() {
return PywbI18N.instance.getWeekDays().map(d => d.substr(0,1));
},
dayStyle() {
const s = this.daySize;
return `height: ${s}px; width: ${s}px; line-height: ${s}px`;
@ -143,6 +146,9 @@ export default {
}
},
methods: {
getLongMonthName(id) {
return PywbI18N.instance.getMonth(id);
},
gotoDay(day, event) {
if (!day || !day.snapshotCount) {
return;

View File

@ -23,7 +23,7 @@
<template>
<div class="full-view">
<h2>{{year.id}} ({{year.snapshotCount}} captures)</h2>
<h2>{{year.id}} ({{ $root._(year.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: year.snapshotCount}) }})</h2>
<div class="months">
<CalendarMonth
v-for="month in year.children"

View File

@ -2,16 +2,17 @@
<div class="timeline">
<div class="period-tooltip" v-show="tooltipPeriod" :style="{left: tooltipPeriodPos.x+'px', top: tooltipPeriodPos.y+'px'}">
<template v-if="tooltipPeriod">
<div v-if="tooltipPeriod.snapshot">View capture on
{{ tooltipPeriod.snapshot.getTimeDateFormatted() }}
<div v-if="tooltipPeriod.snapshot">
{{ $root._('View capture on {date}', {date: tooltipPeriod.snapshot.getTimeDateFormatted()}) }}
</div>
<div v-else-if="tooltipPeriod.snapshotPeriod">View capture on
{{ tooltipPeriod.snapshotPeriod.snapshot.getTimeDateFormatted() }}
<div v-else-if="tooltipPeriod.snapshotPeriod">
{{ $root._('View capture on {date}', {date: tooltipPeriod.snapshotPeriod.snapshot.getTimeDateFormatted()}) }}
</div>
<div v-else-if="tooltipPeriod.snapshotCount">
{{ tooltipPeriod.snapshotCount }} captures
<span v-if="isTooltipPeriodDayOrHour">on</span><span v-else>in</span>
{{ tooltipPeriod.getFullReadableId() }}
{{ $root._(
isTooltipPeriodDayOrHour ? '{capture_text} on {date}':'{capture_text} in {month}', // TODO: split translation into "in {year}" and "in {month}"
{ capture_text: $root._(tooltipPeriod.snapshotCount !== 1 ? '{count} captures' : '{count} capture', {count: tooltipPeriod.snapshotCount}), [isTooltipPeriodDayOrHour ? 'date':'month']: tooltipPeriod.getFullReadableId() } )
}}
</div>
</template>
</div>

View File

@ -11,12 +11,11 @@
<span class="goto" @click="changePeriod(parent)" :title="getPeriodZoomOutText(parent)">
{{parent.getReadableId(true)}}
</span>
&gt;
</span>
</template>
<span class="item">
<span class="current">{{period.getReadableId(true)}}</span>
<span class="count">({{period.snapshotCount}} capture<span v-if="period.snapshotCount !== 1">s</span>)</span>
<span class="count">({{ $root._(period.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: period.snapshotCount}) }})</span>
</span>
</div>
</template>

View File

@ -2,13 +2,13 @@
<div class="timeline-linear">
<div class="title">
<div>{{ period.getFullReadableId() }}</div>
<div>{{ period.snapshotCount }} capture<span v-if="period.snapshotCount > 1">s</span></div>
<div>{{ $root._(period.snapshotCount !== 1 ? '{count} captures':'{count} capture', {count: period.snapshotCount}) }}</div>
</div>
<div class="list">
<div v-for="period in snapshotPeriods">
<span class="link" @click="gotoPeriod(period)" >{{period.snapshot.getTimeFormatted()}}</span>
<span v-if="isCurrentSnapshot(period)" class="current">current</span>
<span v-if="isCurrentSnapshot(period)" class="current">{{$root._('current')}}</span>
</div>
</div>
</div>

51
pywb/vueui/src/i18n.js Normal file
View File

@ -0,0 +1,51 @@
export class PywbI18N {
static #locale = ''; // private (can only be set here)
static getLocale() { // get via public static method
return PywbI18N.#locale;
}
static init = (locale, config) => {
if (PywbI18N.instance) {
throw new Error('cannot instantiate PywbI18N twice');
}
PywbI18N.#locale = locale;
PywbI18N.instance = new PywbI18N(config);
}
// PywbI18N expects from the i18n string source to receive months SHORT and LONG names in the config like this:
// config.jan_short, config.jan_long, ...., config.<mmm>_short, config.<mmm>_long
static monthIdPrefix = {1:"jan", 2:"feb",3:"mar",4:"apr",5:"may",6:"jun",7:"jul",8:"aug",9:"sep",10:"oct",11:"nov",12:"dec"};
/**
*
* @type {PywbI18N|null}
*/
static instance = null;
constructor(config) {
this.config = {...config}; // make a copy of config
}
// can get long (default) or short month string
getMonth(id, type='long') {
return decodeURIComponent(this.config[PywbI18N.monthIdPrefix[id]+'_'+type]);
}
// can get long (default) or short day string or intial
// PywbI18N expects to receive day's initials like:
// config.mon_short, config.tue_long, ...., config.<mmm>_short, config.<mmm>_long
getWeekDay(id, type='long') {
return decodeURIComponent(this.config[id+'_'+type])
}
getWeekDays(type='long') {
return ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'].map(d => this.getWeekDay(d, type));
}
getText(id, embeddedVariableStrings=null) {
const translated = decodeURIComponent(this.config[id] || id);
if (embeddedVariableStrings && id.indexOf('{') >= 0 && id.indexOf('}') >= 0 ) {
return translated.replace(/{(\w+)}/g, (match, stringId) => embeddedVariableStrings[stringId]);
}
return translated
}
_(id, embeddedVariableStrings=null) {
return this.getText(id, embeddedVariableStrings);
}
}

View File

@ -1,13 +1,15 @@
import appData from "./App.vue";
import { PywbData } from "./model.js";
import { PywbI18N } from "./i18n.js";
import Vue from "vue/dist/vue.esm.browser";
// ===========================================================================
export function main(staticPrefix, url, prefix, timestamp, logoUrl) {
const loadingSpinner = new LoadingSpinner(); // bootstrap loading-spinner EARLY ON
export function main(staticPrefix, url, prefix, timestamp, logoUrl, locale, i18nStrings) {
PywbI18N.init(locale, i18nStrings);
const loadingSpinner = new LoadingSpinner({text: PywbI18N.instance?.getText('Loading...')}); // bootstrap loading-spinner EARLY ON
new CDXLoader(staticPrefix, url, prefix, timestamp, logoUrl, loadingSpinner);
}

View File

@ -1,4 +1,5 @@
const PywbMonthLabels = {1:"Jan", 2:"Feb",3:"Mar",4:"Apr",5:"May",6:"Jun",7:"Jul",8:"Aug",9:"Sep",10:"Oct",11:"Nov",12:"Dec"};
import { PywbI18N } from './i18n.js';
const PywbPeriodIdDelimiter = '-';
export function PywbData(rawSnaps) {
const allTimePeriod = new PywbPeriod({type: PywbPeriod.Type.all, id: "all"});
@ -116,15 +117,15 @@ export class PywbSnapshot {
}
getTimeDateFormatted() {
return `${PywbMonthLabels[this.month]} ${this.day}, ${this.year} at ${this.getTimeFormatted()}`;
return new Date(this.year, this.month-1, this.day, this.hour, this.minute, this.second).toLocaleString(PywbI18N.getLocale()).toLowerCase();
}
getDateFormatted() {
return `${this.year}-${PywbMonthLabels[this.month]}-${this.day}`;
return new Date(this.year, this.month-1, this.day).toLocaleDateString(PywbI18N.getLocale()).toLowerCase();
}
getTimeFormatted() {
return (this.hour < 13 ? this.hour : (this.hour % 12)) + ":" + ((this.minute < 10 ? "0":"")+this.minute) + ":" + ((this.second < 10 ? "0":"")+this.second) + " " + (this.hour < 12 ? "am":"pm");
return new Date(2000, 0, 1, this.hour, this.minute, this.second).toLocaleTimeString(PywbI18N.getLocale()).toLowerCase();
}
getParentIds() {
@ -461,7 +462,7 @@ PywbPeriod.prototype.findByFullId = function(fullId) {
}
return found;
};
PywbPeriod.prototype.getFullReadableId = function(hasDayCardinalSuffix) {
PywbPeriod.prototype.getFullReadableId = function() {
// remove "all-time" from parents (getParents(true) when printing readable id (of all parents and currrent
switch (this.type) {
case PywbPeriod.Type.all:
@ -469,36 +470,47 @@ PywbPeriod.prototype.getFullReadableId = function(hasDayCardinalSuffix) {
case PywbPeriod.Type.year:
return this.id;
case PywbPeriod.Type.month:
return this.getReadableId(hasDayCardinalSuffix) + ' ' + this.parent.id;
return this.getReadableId() + ' ' + this.parent.id;
case PywbPeriod.Type.day: {
return this.parent.getReadableId(hasDayCardinalSuffix) + ' ' + this.getReadableId(hasDayCardinalSuffix) + ', ' + this.parent.parent.id;
return new Date(this.parent.parent.id, this.parent.id, this.getReadableId()).toLocaleDateString(PywbI18N.getLocale());
}
case PywbPeriod.Type.hour:
return this.parent.parent.getReadableId(hasDayCardinalSuffix) + ' ' + this.parent.getReadableId(hasDayCardinalSuffix) + ', ' + this.parent.parent.parent.id + ' at ' + this.getReadableId(hasDayCardinalSuffix);
const hourRange = this.getReadableId({hourRange: true});
return this.parent.getFullReadableId() + ' ' + PywbI18N.instance._('from {hour1} to {hour2}', {
hour1: hourRange[0],
hour2: hourRange[1]
});
case PywbPeriod.Type.snapshot:
return this.parent.parent.getReadableId(hasDayCardinalSuffix) + ' ' + this.parent.getReadableId(hasDayCardinalSuffix) + ', ' + this.parent.parent.parent.id + ' at ' + this.snapshot.getTimeFormatted();
return this.snapshot.getTimeDateFormatted();
}
};
PywbPeriod.prototype.getReadableId = function(hasDayCardinalSuffix) {
PywbPeriod.prototype.getReadableId = function(opts={hourRange:null}) {
switch (this.type) {
case PywbPeriod.Type.all:
return "All-time";
return PywbI18N.instance._("All-time");
case PywbPeriod.Type.year:
return this.id;
case PywbPeriod.Type.month:
return PywbMonthLabels[this.id];
return PywbI18N.instance.getMonth(this.id, 'short');
case PywbPeriod.Type.day: {
let suffix = "";
if (hasDayCardinalSuffix) {
const singleDigit = this.id % 10;
const isTens = Math.floor(this.id / 10) === 1;
const suffixes = {1:"st", 2:"nd",3:"rd"};
suffix = (isTens || !suffixes[singleDigit]) ? "th" : suffixes[singleDigit];
}
// DISABLING cardinal suffix for now, as it is complicated to replicate in multiple locales with 1 simple function
// TODO: add cardinal suffix handling later IF REQUESTED!
// if (cardinalSuffix) {
// const singleDigit = this.id % 10;
// const isTens = Math.floor(this.id / 10) === 1;
// const suffixes = {1:"st", 2:"nd",3:"rd"};
// suffix = (isTens || !suffixes[singleDigit]) ? "th" : suffixes[singleDigit];
// }
return this.id + suffix;
}
case PywbPeriod.Type.hour:
return ({1:"12 am", 2: "3 am", 3: "6 am", 4: "9 am", 5: "noon", 6: "3 pm", 7: "6 pm", 8: "9 pm"})[this.id];
// use browser's locale setting to get time string and remove seconds, and lower-case it (in case AM-PM)
const hours = [0, 3, 6, 9, 12, 15, 18, 21].map(hour => new Date(2000, 0, 1, hour, 0, 0).toLocaleTimeString(PywbI18N.getLocale()).replace(/^(\d{1,2}:\d\d):\d\d/, (m, m1)=> m1).toLowerCase());
if (opts.hourRange) {
return [hours[this.id-1], hours[this.id % hours.length]];
}
return hours[this.id-1];
//return ({1:'midnight', 2: '6 am', 3: 'noon', 4: '6 pm'})[this.id];
//return (this.id < 13 ? this.id : this.id % 12) + ' ' + (this.id < 12 ? 'am':'pm');
case PywbPeriod.Type.snapshot: