added translations
This commit is contained in:
1 parent
36ed3880e8
commit
71635eb574
33 files changed
+1025
-241
No files matched your search
@@ -1,6 +1,7 @@
|
||||
// Shared date-range handling for Browse Events and both maps — remembered
|
||||
// across pages the way location.js remembers the place, validated in one
|
||||
// spot, and bound to German-format (dd.mm.yyyy) date pickers. Pages still
|
||||
// spot, and bound to date pickers shown in the page language (format and
|
||||
// calendar names from i18n.js / the flatpickr locale). Pages still
|
||||
// own what a range *does* (a server-side filter on Browse, a client-side
|
||||
// one on the maps); this only owns the range itself.
|
||||
(function (window) {
|
||||
@@ -64,11 +65,12 @@
|
||||
} catch (e) { /* localStorage disabled/unavailable */ }
|
||||
}
|
||||
|
||||
// Turns a pair of <input type="date"> into German-format pickers when
|
||||
// flatpickr is loaded. A native date input displays in the *browser's*
|
||||
// locale and can't be forced to dd.mm.yyyy, so the visible field is
|
||||
// flatpickr's alt input (d.m.Y, German calendar) while the original input
|
||||
// keeps holding the machine-readable "Y-m-d" value everything else reads.
|
||||
// Turns a pair of <input type="date"> into pickers in the page language
|
||||
// when flatpickr is loaded. A native date input displays in the *browser's*
|
||||
// locale and can't be forced to the site's format, so the visible field is
|
||||
// flatpickr's alt input (e.g. d.m.Y with a German calendar) while the
|
||||
// original input keeps holding the machine-readable "Y-m-d" value
|
||||
// everything else reads.
|
||||
// Without flatpickr (e.g. its CDN is unreachable) the native inputs keep
|
||||
// working as they are.
|
||||
//
|
||||
@@ -79,10 +81,12 @@
|
||||
var pickers = [];
|
||||
if (window.flatpickr) {
|
||||
var options = {
|
||||
locale: 'de',
|
||||
// flatpickr ships a file per language (loaded by
|
||||
// _datepicker_scripts.html.twig); English is built in.
|
||||
locale: (window.flatpickr.l10ns && window.flatpickr.l10ns[window.VindnI18n.locale]) ? window.VindnI18n.locale : 'default',
|
||||
dateFormat: 'Y-m-d',
|
||||
altInput: true,
|
||||
altFormat: 'd.m.Y',
|
||||
altFormat: window.VindnI18n.dateFormat,
|
||||
allowInput: true,
|
||||
disableMobile: true, // the native mobile picker would bring the device locale back
|
||||
static: true, // rendered inside the card, so it also shows in a fullscreen map
|
||||
@@ -105,7 +109,7 @@
|
||||
alt.id = el.id + 'Display';
|
||||
if (label) label.setAttribute('for', alt.id);
|
||||
alt.setAttribute('aria-label', el.getAttribute('aria-label') || '');
|
||||
alt.setAttribute('placeholder', 'TT.MM.JJJJ');
|
||||
alt.setAttribute('placeholder', window.VindnI18n.t('dates.placeholder'));
|
||||
return picker;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Browser-side translations. The server prints the catalog for the current
|
||||
// language (translations/js.<locale>.yaml, via the vindn_i18n_json Twig
|
||||
// function in base.html.twig) into window.VINDN_I18N; this module only looks
|
||||
// things up. All visible text in public/js goes through t()/count() so a new
|
||||
// language is a translations/ change, not a code change.
|
||||
(function (window) {
|
||||
var data = window.VINDN_I18N || {};
|
||||
var locale = data.locale || 'de';
|
||||
var messages = data.messages || {};
|
||||
|
||||
var numberFormats = {};
|
||||
var pluralRules;
|
||||
try {
|
||||
pluralRules = new Intl.PluralRules(locale);
|
||||
} catch (e) { /* unknown locale tag — fall back to "other" below */ }
|
||||
|
||||
// "Hello %name%" + {name: 'x'} — same %placeholder% syntax as Symfony's
|
||||
// catalogs. An unknown key shows itself, which is visible but harmless.
|
||||
function t(key, params) {
|
||||
var text = Object.prototype.hasOwnProperty.call(messages, key) ? messages[key] : key;
|
||||
if (params) {
|
||||
Object.keys(params).forEach(function (name) {
|
||||
text = text.split('%' + name + '%').join(params[name]);
|
||||
});
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// Number in the page language, at most `decimals` fraction digits.
|
||||
function number(value, decimals) {
|
||||
decimals = decimals || 0;
|
||||
if (!numberFormats[decimals]) {
|
||||
numberFormats[decimals] = new Intl.NumberFormat(locale, { maximumFractionDigits: decimals });
|
||||
}
|
||||
return numberFormats[decimals].format(value);
|
||||
}
|
||||
|
||||
// Plural-aware message: looks up "<key>.<rule>" (one, other, few, …) for
|
||||
// the language's own plural rules, falling back to "<key>.other". The
|
||||
// count itself is available as %n%, already formatted.
|
||||
function count(key, n, params) {
|
||||
var rule = pluralRules ? pluralRules.select(n) : 'other';
|
||||
var full = key + '.' + rule;
|
||||
if (!Object.prototype.hasOwnProperty.call(messages, full)) full = key + '.other';
|
||||
return t(full, Object.assign({ n: number(n) }, params || {}));
|
||||
}
|
||||
|
||||
window.VindnI18n = {
|
||||
locale: locale,
|
||||
dateFormat: data.dateFormat || 'd.m.Y',
|
||||
t: t,
|
||||
number: number,
|
||||
count: count
|
||||
};
|
||||
})(window);
|
||||
@@ -64,7 +64,7 @@
|
||||
}
|
||||
if (callbacks.onStart) callbacks.onStart();
|
||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
||||
if (callbacks.onFound) callbacks.onFound(pos.coords.latitude, pos.coords.longitude, 'Your location');
|
||||
if (callbacks.onFound) callbacks.onFound(pos.coords.latitude, pos.coords.longitude, window.VindnI18n.t('location.yours'));
|
||||
}, function () {
|
||||
if (callbacks.onError) callbacks.onError();
|
||||
}, { timeout: 8000 });
|
||||
|
||||
+39
-28
@@ -30,14 +30,11 @@
|
||||
statusEl.classList.toggle('is-error', !!isError);
|
||||
}
|
||||
|
||||
// German number formatting for what the panel shows (decimal comma, dot
|
||||
// as thousands separator) — mirrors GermanFormatExtension::distance() on
|
||||
// the server so a distance reads the same on a card and in the panel.
|
||||
var DE_INT = new Intl.NumberFormat('de-DE', { maximumFractionDigits: 0 });
|
||||
var DE_ONE_DECIMAL = new Intl.NumberFormat('de-DE', { maximumFractionDigits: 1 });
|
||||
|
||||
// Number formatting for what the panel shows (separators of the page
|
||||
// language) — mirrors LocaleFormatExtension::distance() on the server so a
|
||||
// distance reads the same on a card and in the panel.
|
||||
function formatKm(km) {
|
||||
return km < 10 ? DE_ONE_DECIMAL.format(km) : DE_INT.format(km);
|
||||
return window.VindnI18n.number(km, km < 10 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Day-range filtering for the maps — the same "event overlaps [from, to]"
|
||||
@@ -260,7 +257,7 @@
|
||||
(ev.category ? '<span class="cat" style="color:' + hex + ';">' + ev.category + '</span>' : '') +
|
||||
'<h4>' + ev.title + '</h4>' +
|
||||
(ev.venue ? '<p>' + ev.venue + '</p>' : '') +
|
||||
(ev.url ? '<a href="' + ev.url + '">View event →</a>' : '') +
|
||||
(ev.url ? '<a href="' + ev.url + '">' + window.VindnI18n.t('map.view_event') + '</a>' : '') +
|
||||
'</div>';
|
||||
marker.bindPopup(popupHtml);
|
||||
if (onMarkerClick) marker.on('click', function () { onMarkerClick(ev, marker); });
|
||||
@@ -285,7 +282,7 @@
|
||||
|
||||
var center = opts.center || (events.length ? [events[0].lat, events[0].lon] : DEFAULT_CENTER);
|
||||
var map = L.map(containerId, {
|
||||
zoomControl: !!opts.zoomControl,
|
||||
zoomControl: false, // added below, so its tooltips can be translated
|
||||
attributionControl: !!opts.attributionControl,
|
||||
scrollWheelZoom: !!opts.scrollWheelZoom,
|
||||
// A raster L.tileLayer used to supply maxZoom implicitly; the
|
||||
@@ -299,9 +296,23 @@
|
||||
// Requires two-finger drag / Ctrl+scroll to pan/zoom, with an
|
||||
// on-map hint, so an embedded map (home hero, mid-page /map) doesn't
|
||||
// trap page scroll/pinch-zoom on touch devices.
|
||||
gestureHandling: opts.gestureHandling !== false
|
||||
gestureHandling: opts.gestureHandling !== false,
|
||||
gestureHandlingOptions: {
|
||||
text: {
|
||||
touch: window.VindnI18n.t('map.gesture.touch'),
|
||||
scroll: window.VindnI18n.t('map.gesture.scroll'),
|
||||
scrollMac: window.VindnI18n.t('map.gesture.scroll_mac')
|
||||
}
|
||||
}
|
||||
}).setView(center, opts.zoom || 13);
|
||||
|
||||
if (opts.zoomControl) {
|
||||
L.control.zoom({
|
||||
zoomInTitle: window.VindnI18n.t('map.zoom_in'),
|
||||
zoomOutTitle: window.VindnI18n.t('map.zoom_out')
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
if (L.maplibreGL) {
|
||||
// OpenFreeMap serves vector tiles only, rendered by MapLibre GL and
|
||||
// bridged into this Leaflet map as an ordinary layer — everything
|
||||
@@ -331,8 +342,8 @@
|
||||
// just the bare map div — those overlay controls are DOM siblings of
|
||||
// #mapCanvas, not descendants, so they'd vanish in fullscreen otherwise.
|
||||
L.control.fullscreen({
|
||||
title: 'View fullscreen',
|
||||
titleCancel: 'Exit fullscreen',
|
||||
title: window.VindnI18n.t('map.fullscreen'),
|
||||
titleCancel: window.VindnI18n.t('map.fullscreen_exit'),
|
||||
fullscreenElement: el.closest('.stage') || el
|
||||
}).addTo(map);
|
||||
|
||||
@@ -493,7 +504,7 @@
|
||||
var list = currentList();
|
||||
|
||||
var countEl = document.getElementById('resultCount');
|
||||
if (countEl) countEl.textContent = DE_INT.format(list.length) + (list.length === 1 ? ' event' : ' events');
|
||||
if (countEl) countEl.textContent = window.VindnI18n.count('map.count', list.length);
|
||||
|
||||
var panelEl = document.getElementById('panelList');
|
||||
if (!panelEl) return;
|
||||
@@ -546,7 +557,7 @@
|
||||
}
|
||||
|
||||
function statusForLimit() {
|
||||
return limit ? ('Showing the ' + limit + ' nearest events') : 'Showing events near you';
|
||||
return limit ? window.VindnI18n.t('map.status_nearest', { limit: limit }) : window.VindnI18n.t('map.status_near_you');
|
||||
}
|
||||
|
||||
function setUserLocation(lat, lon, statusText, placeName) {
|
||||
@@ -572,15 +583,15 @@
|
||||
function requestLocation(isAutoCheck) {
|
||||
var statusEl = document.getElementById('locateStatus');
|
||||
window.VindnLocation.useMyLocation({
|
||||
onUnsupported: function () { setLocateStatus(statusEl, 'Location not supported by your browser', true); },
|
||||
onStart: function () { setLocateStatus(statusEl, 'Finding you…', false); },
|
||||
onUnsupported: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.unsupported'), true); },
|
||||
onStart: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.finding'), false); },
|
||||
onFound: function (lat, lon, name) {
|
||||
// A manual search/click may have already happened while this
|
||||
// (silent, permission-check-triggered) lookup was still resolving.
|
||||
if (isAutoCheck && manualLocationSet) return;
|
||||
setUserLocation(lat, lon, statusForLimit(), name);
|
||||
},
|
||||
onError: function () { setLocateStatus(statusEl, "Couldn't get your location", true); }
|
||||
onError: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.failed'), true); }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -597,10 +608,10 @@
|
||||
manualLocationSet = true;
|
||||
var statusEl = document.getElementById('locateStatus');
|
||||
window.VindnLocation.searchPlace(query, {
|
||||
onStart: function () { setLocateStatus(statusEl, 'Looking up "' + query + '"…', false); },
|
||||
onNotFound: function () { setLocateStatus(statusEl, 'Could not find "' + query + '"', true); },
|
||||
onStart: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.looking_up', { query: query }), false); },
|
||||
onNotFound: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.not_found', { query: query }), true); },
|
||||
onFound: function (lat, lon, name) { setUserLocation(lat, lon, statusForLimit(), name); },
|
||||
onError: function () { setLocateStatus(statusEl, 'Location search failed', true); }
|
||||
onError: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.search_failed'), true); }
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -745,10 +756,10 @@
|
||||
}
|
||||
|
||||
var statusEl = document.getElementById('locateStatus');
|
||||
setLocateStatus(statusEl, 'Showing events within ' + state.rangeKm + ' km', false);
|
||||
setLocateStatus(statusEl, window.VindnI18n.t('map.status_within', { km: state.rangeKm }), false);
|
||||
|
||||
var countEl = document.getElementById('resultCount');
|
||||
if (countEl) countEl.textContent = DE_INT.format(visible.length) + (visible.length === 1 ? ' event' : ' events');
|
||||
if (countEl) countEl.textContent = window.VindnI18n.count('map.count', visible.length);
|
||||
|
||||
var panelEl = document.getElementById('panelList');
|
||||
if (panelEl) {
|
||||
@@ -855,10 +866,10 @@
|
||||
manualLocationSet = true;
|
||||
var statusEl = document.getElementById('locateStatus');
|
||||
window.VindnLocation.searchPlace(query, {
|
||||
onStart: function () { setLocateStatus(statusEl, 'Looking up "' + query + '"…', false); },
|
||||
onNotFound: function () { setLocateStatus(statusEl, 'Could not find "' + query + '"', true); },
|
||||
onStart: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.looking_up', { query: query }), false); },
|
||||
onNotFound: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.not_found', { query: query }), true); },
|
||||
onFound: function (lat, lon, name) { setSource(lat, lon, name); },
|
||||
onError: function () { setLocateStatus(statusEl, 'Location search failed', true); }
|
||||
onError: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.search_failed'), true); }
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -866,15 +877,15 @@
|
||||
function requestLocation(isAutoCheck) {
|
||||
var statusEl = document.getElementById('locateStatus');
|
||||
window.VindnLocation.useMyLocation({
|
||||
onUnsupported: function () { setLocateStatus(statusEl, 'Location not supported by your browser', true); },
|
||||
onStart: function () { setLocateStatus(statusEl, 'Finding you…', false); },
|
||||
onUnsupported: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.unsupported'), true); },
|
||||
onStart: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.finding'), false); },
|
||||
onFound: function (lat, lon, name) {
|
||||
// A manual search/click/drag may have already happened while this
|
||||
// (silent, permission-check-triggered) lookup was still resolving.
|
||||
if (isAutoCheck && manualLocationSet) return;
|
||||
setSource(lat, lon, name);
|
||||
},
|
||||
onError: function () { setLocateStatus(statusEl, "Couldn't get your location", true); }
|
||||
onError: function () { setLocateStatus(statusEl, window.VindnI18n.t('location.failed'), true); }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,10 @@ h1,h2,h3{ font-family: var(--f-display); font-weight:700; letter-spacing:-0.01em
|
||||
border:none;
|
||||
}
|
||||
.nav-actions{ display:flex; align-items:center; gap:16px; }
|
||||
.lang-switch{ display:flex; align-items:center; gap:2px; font-family:var(--f-mono); font-size:12px; }
|
||||
.lang-switch a, .lang-switch span{ padding:5px 7px; border-radius:var(--r-pill); color:var(--muted); }
|
||||
.lang-switch a:hover{ color:var(--ink); }
|
||||
.lang-switch .is-active{ background:var(--ink); color:var(--paper); }
|
||||
.nav-burger{ display:none; background:none; border:none; padding:6px; }
|
||||
.nav-burger span{ display:block; width:22px; height:2px; background:var(--ink); margin:5px 0; transition:transform .2s ease, opacity .2s ease; }
|
||||
.nav-burger.is-open span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
|
||||
|
||||
Reference in new issue
Block a user