added translations

This commit is contained in:
Hendrik Behme committed 2026-10-09 16:37:53 +02:00
1 parent 36ed3880e8
commit 71635eb574
33 files changed
+1025 -241

No files matched your search

+13 -9
View File
@@ -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;
});
}
+55
View File
@@ -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);
+1 -1
View File
@@ -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
View File
@@ -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); }
});
}