Files
deskhub/desktop/src/i18n.js
T
2026-10-24 20:15:40 +00:00

103 lines
5.6 KiB
JavaScript

'use strict';
/**
* Sprachen des Windows-Programms: Deutsch (Quelltext) und English.
* Der Quelltext ist deutsch; bei English werden alle Texte beim Aufbau der Oberfläche über das Wörterbuch (lang-en.js) übersetzt:
* Textknoten, Attribute (placeholder, title …), textContent, Meldungen (toast), confirm/alert und Benachrichtigungen.
* Schlüssel = deutscher Text; "{}" = beliebiger Teil, "{#}" = Zahl, "{1}"/"{2}" = Reihenfolge in der Übersetzung. Was fehlt, bleibt deutsch.
* Die Wahl steht in localStorage ("lang") und wird nach dem Umschalten (Klick auf eine Flagge) durch Neuladen übernommen.
*/
(function () {
const stored = (() => { try { return localStorage.getItem('lang'); } catch { return null; } })();
const lang = stored === 'de' || stored === 'en' ? stored : ((navigator.language || 'de').toLowerCase().startsWith('de') ? 'de' : 'en');
const FLAG = {
de: 'data:image/svg+xml;base64,' + btoa('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3"><rect width="5" height="3" fill="#FFCE00"/><rect width="5" height="2" fill="#DD0000"/><rect width="5" height="1"/></svg>'),
en: 'data:image/svg+xml;base64,' + btoa('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30"><clipPath id="s"><path d="M0,0 v30 h60 v-30 z"/></clipPath><clipPath id="t"><path d="M30,15 h30 v15 z v15 h-30 z h-30 v-15 z v-15 h30 z"/></clipPath><g clip-path="url(#s)"><path d="M0,0 v30 h60 v-30 z" fill="#012169"/><path d="M0,0 L60,30 M60,0 L0,30" stroke="#fff" stroke-width="6"/><path d="M0,0 L60,30 M60,0 L0,30" clip-path="url(#t)" stroke="#C8102E" stroke-width="4"/><path d="M30,0 v30 M0,15 h60" stroke="#fff" stroke-width="10"/><path d="M30,0 v30 M0,15 h60" stroke="#C8102E" stroke-width="6"/></g></svg>'),
};
const NAMES = { de: 'Deutsch', en: 'English' };
const exact = new Map();
const patterns = [];
const norm = (s) => String(s).replace(/\s+/g, ' ').trim();
if (lang === 'en' && window.I18N_EN) {
for (const [k, v] of Object.entries(window.I18N_EN)) {
if (!v || k[0] === '_') continue;
const key = norm(k);
if (key.includes('{}') || key.includes('{#}')) {
const parts = key.split(/(\{#\}|\{\})/);
const re = '^' + parts.map((p, i) => (i % 2 === 0 ? p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : (p === '{#}' ? '(\\d+)' : '(.+?)'))).join('') + '$';
patterns.push([new RegExp(re, 's'), v]);
} else exact.set(key, v);
}
}
// Übersetzt einen Text; Leerraum am Rand bleibt erhalten, unbekannte Texte bleiben unverändert
function t(s) {
if (lang !== 'en' || typeof s !== 'string' || s === '') return s;
const m = /^(\s*)([\s\S]*?)(\s*)$/.exec(s);
const key = norm(m[2]);
if (!key) return s;
let r = exact.get(key);
if (r === undefined) {
for (const [re, v] of patterns) {
const mm = re.exec(key);
if (mm) {
let i = 0;
// Eingefügte Teile werden selbst noch einmal übersetzt (zum Beispiel „in 2 Std 5 Min“ in „Lösung fällig: {}“)
r = v.replace(/\{(\d+)\}/g, (_, n) => t(mm[+n] ?? '')).replace(/\{\}/g, () => t(mm[++i] ?? ''));
break;
}
}
}
return r === undefined ? s : m[1] + r + m[3];
}
if (lang === 'en') {
// Textknoten, die über createTextNode oder append() entstehen
const ct = Document.prototype.createTextNode;
Document.prototype.createTextNode = function (d) { return ct.call(this, t(String(d))); };
for (const name of ['append', 'prepend']) {
const orig = Element.prototype[name];
Element.prototype[name] = function (...a) { return orig.apply(this, a.map((x) => (typeof x === 'string' ? t(x) : x))); };
}
const sa = Element.prototype.setAttribute;
const attrs = new Set(['placeholder', 'title', 'aria-label', 'alt']);
Element.prototype.setAttribute = function (n, v) { return sa.call(this, n, attrs.has(n) && typeof v === 'string' ? t(v) : v); };
const td = Object.getOwnPropertyDescriptor(Node.prototype, 'textContent');
Object.defineProperty(Node.prototype, 'textContent', { ...td, set(v) { td.set.call(this, typeof v === 'string' ? t(v) : v); } });
const ph = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'placeholder');
if (ph) Object.defineProperty(HTMLInputElement.prototype, 'placeholder', { ...ph, set(v) { ph.set.call(this, t(String(v))); } });
for (const fn of ['confirm', 'alert']) { const o = window[fn].bind(window); window[fn] = (m) => o(t(String(m))); }
document.documentElement.lang = 'en';
}
// Sprachwechsler mit Flaggen (gleiche Optik wie im Web)
function switcher() {
const box = document.createElement('span');
box.className = 'lang-switch';
box.setAttribute('role', 'group');
for (const code of ['de', 'en']) {
const a = document.createElement('button');
a.type = 'button';
a.className = 'lang-flag' + (code === lang ? ' on' : '');
a.title = NAMES[code];
a.setAttribute('aria-label', NAMES[code]);
if (code === lang) a.setAttribute('aria-current', 'true');
const img = document.createElement('img');
img.src = FLAG[code]; img.alt = ''; img.width = 24; img.height = 16;
a.appendChild(img);
a.addEventListener('click', () => { if (code !== lang) setLang(code); });
box.appendChild(a);
}
return box;
}
function setLang(code) {
try { localStorage.setItem('lang', code); } catch {}
try { window.deskhub?.setSettings?.({ language: code }); } catch {}
location.reload();
}
try { window.deskhub?.setSettings?.({ language: lang }); } catch {}
window.I18N = { lang, t, switcher, setLang, locale: lang === 'en' ? 'en-GB' : 'de-DE' };
window.tr = t;
})();