feat: 7 languages — EN, DE, FR, IT, ES, PT, RU

- Full translations for all UI strings in 7 languages
- Language picker shows native names (Français, Italiano, Español, Português, Русский)
- Auto-detection via navigator.languages
- Cyrillic font subset for Russian (Inter, 19KB)
- Footer shared across all languages (untranslated links)
This commit is contained in:
Alexander Schmidt
2026-03-25 18:15:07 +01:00
parent 4c93e335f3
commit e7f3451f82
5 changed files with 302 additions and 58 deletions

BIN
fonts/inter-cyrillic.woff2 Normal file

Binary file not shown.

347
i18n.js
View File

@@ -2,58 +2,18 @@ var I18n = (function () {
'use strict'; 'use strict';
var languages = { var languages = {
de: { name: 'Deutsch', flag: 'DE' }, en: { name: 'English' },
en: { name: 'English', flag: 'EN' } de: { name: 'Deutsch' },
fr: { name: 'Français' },
it: { name: 'Italiano' },
es: { name: 'Español' },
pt: { name: 'Português' },
ru: { name: 'Русский' }
}; };
var footer = 'Open Source &middot; No Backend &middot; No KYC &middot; <a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a>';
var translations = { var translations = {
de: {
subtitle: 'Monero-Zahlungsanforderung in Sekunden',
label_addr: 'XMR-Adresse',
placeholder_addr: '8...',
label_amount: 'Betrag',
label_desc: 'Beschreibung (optional)',
placeholder_desc: 'z.B. Rechnung #42, Freelance-Arbeit...',
label_timer: 'Zahlungsfrist (optional)',
days: 'Tage',
placeholder_timer_custom: 'Tage',
btn_generate: 'Zahlungsanforderung erstellen',
btn_open_wallet: 'In Wallet öffnen',
btn_copy_addr: 'Adresse kopieren',
btn_download_pdf: 'PDF Rechnung',
pdf_title: 'Zahlungsanforderung',
pdf_address: 'XMR-Adresse',
pdf_amount: 'Betrag',
pdf_desc: 'Beschreibung',
pdf_deadline: 'Zahlungsfrist',
pdf_deadline_days: '{d} Tage',
pdf_date: 'Datum',
pdf_scan_qr: 'QR-Code scannen zum Bezahlen',
pdf_footer: 'Erstellt mit xmrpay.link — Keine Registrierung, kein KYC',
qr_hint: 'Klick auf QR zum Speichern',
footer: 'Open Source &middot; Kein Backend &middot; Kein KYC &middot; <a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a>',
aria_currency: 'Währung',
label_uri_details: 'Monero-URI anzeigen',
label_share_link: 'Teilbarer Link',
btn_new_request: 'Neue Zahlungsanforderung',
toast_copied: 'Kopiert!',
countdown_expired: 'Zahlungsfrist abgelaufen',
countdown_remaining_days: 'Zahlungsfrist: {d} Tage, {h} Std.',
countdown_remaining_hours: 'Zahlungsfrist: {h}:{m} Std.',
rates_offline: 'Kurse nicht verfügbar — nur XMR-Betrag möglich',
btn_prove_payment: 'Zahlung nachweisen',
label_tx_hash: 'Transaction ID (TX Hash)',
placeholder_tx_hash: '64 Hex-Zeichen...',
label_tx_key: 'Transaction Key (TX Key)',
placeholder_tx_key: '64 Hex-Zeichen...',
btn_verify_proof: 'Zahlung verifizieren',
proof_verifying: 'Verifiziere...',
proof_verified: 'Zahlung bestätigt: {amount} XMR',
proof_no_match: 'Kein passender Output gefunden — TX Key oder Adresse stimmt nicht',
proof_tx_not_found: 'Transaktion nicht gefunden',
proof_error: 'Fehler bei der Verifizierung',
status_paid: 'Bezahlt'
},
en: { en: {
subtitle: 'Monero payment request in seconds', subtitle: 'Monero payment request in seconds',
label_addr: 'XMR Address', label_addr: 'XMR Address',
@@ -76,11 +36,10 @@ var I18n = (function () {
pdf_deadline_days: '{d} days', pdf_deadline_days: '{d} days',
pdf_date: 'Date', pdf_date: 'Date',
pdf_scan_qr: 'Scan QR code to pay', pdf_scan_qr: 'Scan QR code to pay',
pdf_footer: 'Created with xmrpay.link — No registration, no KYC', pdf_footer: 'Created with xmrpay.link',
qr_hint: 'Click QR to save', qr_hint: 'Click QR to save',
footer: 'Open Source &middot; No Backend &middot; No KYC &middot; <a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank">Source</a> &middot; <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a>', footer: footer,
aria_currency: 'Currency', aria_currency: 'Currency',
label_uri_details: 'Show Monero URI',
label_share_link: 'Shareable link', label_share_link: 'Shareable link',
btn_new_request: 'New payment request', btn_new_request: 'New payment request',
toast_copied: 'Copied!', toast_copied: 'Copied!',
@@ -96,21 +55,297 @@ var I18n = (function () {
btn_verify_proof: 'Verify payment', btn_verify_proof: 'Verify payment',
proof_verifying: 'Verifying...', proof_verifying: 'Verifying...',
proof_verified: 'Payment confirmed: {amount} XMR', proof_verified: 'Payment confirmed: {amount} XMR',
proof_no_match: 'No matching output found — TX key or address mismatch', proof_no_match: 'No matching output — TX key or address mismatch',
proof_tx_not_found: 'Transaction not found', proof_tx_not_found: 'Transaction not found',
proof_error: 'Verification error', proof_error: 'Verification error',
status_paid: 'Paid' status_paid: 'Paid'
},
de: {
subtitle: 'Monero-Zahlungsanforderung in Sekunden',
label_addr: 'XMR-Adresse',
placeholder_addr: '8...',
label_amount: 'Betrag',
label_desc: 'Beschreibung (optional)',
placeholder_desc: 'z.B. Rechnung #42, Freelance-Arbeit...',
label_timer: 'Zahlungsfrist (optional)',
days: 'Tage',
placeholder_timer_custom: 'Tage',
btn_generate: 'Zahlungsanforderung erstellen',
btn_open_wallet: 'In Wallet öffnen',
btn_copy_addr: 'Adresse kopieren',
btn_download_pdf: 'PDF Rechnung',
pdf_title: 'Zahlungsanforderung',
pdf_address: 'XMR-Adresse',
pdf_amount: 'Betrag',
pdf_desc: 'Beschreibung',
pdf_deadline: 'Zahlungsfrist',
pdf_deadline_days: '{d} Tage',
pdf_date: 'Datum',
pdf_scan_qr: 'QR-Code scannen zum Bezahlen',
pdf_footer: 'Erstellt mit xmrpay.link',
qr_hint: 'Klick auf QR zum Speichern',
footer: footer,
aria_currency: 'Währung',
label_share_link: 'Teilbarer Link',
btn_new_request: 'Neue Zahlungsanforderung',
toast_copied: 'Kopiert!',
countdown_expired: 'Zahlungsfrist abgelaufen',
countdown_remaining_days: 'Zahlungsfrist: {d} Tage, {h} Std.',
countdown_remaining_hours: 'Zahlungsfrist: {h}:{m} Std.',
rates_offline: 'Kurse nicht verfügbar — nur XMR-Betrag möglich',
btn_prove_payment: 'Zahlung nachweisen',
label_tx_hash: 'Transaction ID (TX Hash)',
placeholder_tx_hash: '64 Hex-Zeichen...',
label_tx_key: 'Transaction Key (TX Key)',
placeholder_tx_key: '64 Hex-Zeichen...',
btn_verify_proof: 'Zahlung verifizieren',
proof_verifying: 'Verifiziere...',
proof_verified: 'Zahlung bestätigt: {amount} XMR',
proof_no_match: 'Kein passender Output — TX Key oder Adresse stimmt nicht',
proof_tx_not_found: 'Transaktion nicht gefunden',
proof_error: 'Fehler bei der Verifizierung',
status_paid: 'Bezahlt'
},
fr: {
subtitle: 'Demande de paiement Monero en quelques secondes',
label_addr: 'Adresse XMR',
placeholder_addr: '8...',
label_amount: 'Montant',
label_desc: 'Description (facultatif)',
placeholder_desc: 'ex. Facture #42, travail freelance...',
label_timer: 'Date limite de paiement (facultatif)',
days: 'jours',
placeholder_timer_custom: 'Jours',
btn_generate: 'Créer une demande de paiement',
btn_open_wallet: 'Ouvrir dans le wallet',
btn_copy_addr: 'Copier l\'adresse',
btn_download_pdf: 'Facture PDF',
pdf_title: 'Demande de paiement',
pdf_address: 'Adresse XMR',
pdf_amount: 'Montant',
pdf_desc: 'Description',
pdf_deadline: 'Date limite de paiement',
pdf_deadline_days: '{d} jours',
pdf_date: 'Date',
pdf_scan_qr: 'Scanner le QR code pour payer',
pdf_footer: 'Créé avec xmrpay.link',
qr_hint: 'Cliquez sur le QR pour enregistrer',
footer: footer,
aria_currency: 'Devise',
label_share_link: 'Lien partageable',
btn_new_request: 'Nouvelle demande de paiement',
toast_copied: 'Copié !',
countdown_expired: 'Délai de paiement expiré',
countdown_remaining_days: 'Délai : {d} jours, {h} h',
countdown_remaining_hours: 'Délai : {h}:{m} h',
rates_offline: 'Taux indisponibles — montant en XMR uniquement',
btn_prove_payment: 'Prouver le paiement',
label_tx_hash: 'Transaction ID (TX Hash)',
placeholder_tx_hash: '64 caractères hexadécimaux...',
label_tx_key: 'Transaction Key (TX Key)',
placeholder_tx_key: '64 caractères hexadécimaux...',
btn_verify_proof: 'Vérifier le paiement',
proof_verifying: 'Vérification...',
proof_verified: 'Paiement confirmé : {amount} XMR',
proof_no_match: 'Aucun output correspondant — TX Key ou adresse incorrecte',
proof_tx_not_found: 'Transaction introuvable',
proof_error: 'Erreur de vérification',
status_paid: 'Payé'
},
it: {
subtitle: 'Richiesta di pagamento Monero in pochi secondi',
label_addr: 'Indirizzo XMR',
placeholder_addr: '8...',
label_amount: 'Importo',
label_desc: 'Descrizione (facoltativo)',
placeholder_desc: 'es. Fattura #42, lavoro freelance...',
label_timer: 'Scadenza pagamento (facoltativo)',
days: 'giorni',
placeholder_timer_custom: 'Giorni',
btn_generate: 'Crea richiesta di pagamento',
btn_open_wallet: 'Apri nel wallet',
btn_copy_addr: 'Copia indirizzo',
btn_download_pdf: 'Fattura PDF',
pdf_title: 'Richiesta di pagamento',
pdf_address: 'Indirizzo XMR',
pdf_amount: 'Importo',
pdf_desc: 'Descrizione',
pdf_deadline: 'Scadenza pagamento',
pdf_deadline_days: '{d} giorni',
pdf_date: 'Data',
pdf_scan_qr: 'Scansiona il QR per pagare',
pdf_footer: 'Creato con xmrpay.link',
qr_hint: 'Clicca sul QR per salvare',
footer: footer,
aria_currency: 'Valuta',
label_share_link: 'Link condivisibile',
btn_new_request: 'Nuova richiesta di pagamento',
toast_copied: 'Copiato!',
countdown_expired: 'Scadenza pagamento superata',
countdown_remaining_days: 'Scadenza: {d} giorni, {h} ore',
countdown_remaining_hours: 'Scadenza: {h}:{m} ore',
rates_offline: 'Tassi non disponibili — solo importo in XMR',
btn_prove_payment: 'Dimostra pagamento',
label_tx_hash: 'Transaction ID (TX Hash)',
placeholder_tx_hash: '64 caratteri esadecimali...',
label_tx_key: 'Transaction Key (TX Key)',
placeholder_tx_key: '64 caratteri esadecimali...',
btn_verify_proof: 'Verifica pagamento',
proof_verifying: 'Verifica in corso...',
proof_verified: 'Pagamento confermato: {amount} XMR',
proof_no_match: 'Nessun output corrispondente — TX Key o indirizzo errato',
proof_tx_not_found: 'Transazione non trovata',
proof_error: 'Errore di verifica',
status_paid: 'Pagato'
},
es: {
subtitle: 'Solicitud de pago Monero en segundos',
label_addr: 'Dirección XMR',
placeholder_addr: '8...',
label_amount: 'Monto',
label_desc: 'Descripción (opcional)',
placeholder_desc: 'ej. Factura #42, trabajo freelance...',
label_timer: 'Plazo de pago (opcional)',
days: 'días',
placeholder_timer_custom: 'Días',
btn_generate: 'Crear solicitud de pago',
btn_open_wallet: 'Abrir en wallet',
btn_copy_addr: 'Copiar dirección',
btn_download_pdf: 'Factura PDF',
pdf_title: 'Solicitud de pago',
pdf_address: 'Dirección XMR',
pdf_amount: 'Monto',
pdf_desc: 'Descripción',
pdf_deadline: 'Plazo de pago',
pdf_deadline_days: '{d} días',
pdf_date: 'Fecha',
pdf_scan_qr: 'Escanear QR para pagar',
pdf_footer: 'Creado con xmrpay.link',
qr_hint: 'Clic en QR para guardar',
footer: footer,
aria_currency: 'Moneda',
label_share_link: 'Enlace compartible',
btn_new_request: 'Nueva solicitud de pago',
toast_copied: '¡Copiado!',
countdown_expired: 'Plazo de pago vencido',
countdown_remaining_days: 'Plazo: {d} días, {h} h',
countdown_remaining_hours: 'Plazo: {h}:{m} h',
rates_offline: 'Tasas no disponibles — solo monto en XMR',
btn_prove_payment: 'Demostrar pago',
label_tx_hash: 'Transaction ID (TX Hash)',
placeholder_tx_hash: '64 caracteres hexadecimales...',
label_tx_key: 'Transaction Key (TX Key)',
placeholder_tx_key: '64 caracteres hexadecimales...',
btn_verify_proof: 'Verificar pago',
proof_verifying: 'Verificando...',
proof_verified: 'Pago confirmado: {amount} XMR',
proof_no_match: 'Ningún output coincidente — TX Key o dirección incorrecta',
proof_tx_not_found: 'Transacción no encontrada',
proof_error: 'Error de verificación',
status_paid: 'Pagado'
},
pt: {
subtitle: 'Pedido de pagamento Monero em segundos',
label_addr: 'Endereço XMR',
placeholder_addr: '8...',
label_amount: 'Valor',
label_desc: 'Descrição (opcional)',
placeholder_desc: 'ex. Fatura #42, trabalho freelance...',
label_timer: 'Prazo de pagamento (opcional)',
days: 'dias',
placeholder_timer_custom: 'Dias',
btn_generate: 'Criar pedido de pagamento',
btn_open_wallet: 'Abrir na wallet',
btn_copy_addr: 'Copiar endereço',
btn_download_pdf: 'Fatura PDF',
pdf_title: 'Pedido de pagamento',
pdf_address: 'Endereço XMR',
pdf_amount: 'Valor',
pdf_desc: 'Descrição',
pdf_deadline: 'Prazo de pagamento',
pdf_deadline_days: '{d} dias',
pdf_date: 'Data',
pdf_scan_qr: 'Digitalizar QR para pagar',
pdf_footer: 'Criado com xmrpay.link',
qr_hint: 'Clique no QR para guardar',
footer: footer,
aria_currency: 'Moeda',
label_share_link: 'Link partilhável',
btn_new_request: 'Novo pedido de pagamento',
toast_copied: 'Copiado!',
countdown_expired: 'Prazo de pagamento expirado',
countdown_remaining_days: 'Prazo: {d} dias, {h} h',
countdown_remaining_hours: 'Prazo: {h}:{m} h',
rates_offline: 'Taxas indisponíveis — apenas valor em XMR',
btn_prove_payment: 'Comprovar pagamento',
label_tx_hash: 'Transaction ID (TX Hash)',
placeholder_tx_hash: '64 caracteres hexadecimais...',
label_tx_key: 'Transaction Key (TX Key)',
placeholder_tx_key: '64 caracteres hexadecimais...',
btn_verify_proof: 'Verificar pagamento',
proof_verifying: 'A verificar...',
proof_verified: 'Pagamento confirmado: {amount} XMR',
proof_no_match: 'Nenhum output correspondente — TX Key ou endereço incorreto',
proof_tx_not_found: 'Transação não encontrada',
proof_error: 'Erro de verificação',
status_paid: 'Pago'
},
ru: {
subtitle: 'Запрос на оплату Monero за секунды',
label_addr: 'Адрес XMR',
placeholder_addr: '8...',
label_amount: 'Сумма',
label_desc: 'Описание (необязательно)',
placeholder_desc: 'напр. Счёт #42, фриланс...',
label_timer: 'Срок оплаты (необязательно)',
days: 'дней',
placeholder_timer_custom: 'Дней',
btn_generate: 'Создать запрос на оплату',
btn_open_wallet: 'Открыть в кошельке',
btn_copy_addr: 'Копировать адрес',
btn_download_pdf: 'PDF счёт',
pdf_title: 'Запрос на оплату',
pdf_address: 'Адрес XMR',
pdf_amount: 'Сумма',
pdf_desc: 'Описание',
pdf_deadline: 'Срок оплаты',
pdf_deadline_days: '{d} дней',
pdf_date: 'Дата',
pdf_scan_qr: 'Сканируйте QR для оплаты',
pdf_footer: 'Создано с помощью xmrpay.link',
qr_hint: 'Нажмите на QR для сохранения',
footer: footer,
aria_currency: 'Валюта',
label_share_link: 'Ссылка для отправки',
btn_new_request: 'Новый запрос на оплату',
toast_copied: 'Скопировано!',
countdown_expired: 'Срок оплаты истёк',
countdown_remaining_days: 'Срок: {d} дней, {h} ч',
countdown_remaining_hours: 'Срок: {h}:{m} ч',
rates_offline: 'Курсы недоступны — только сумма в XMR',
btn_prove_payment: 'Подтвердить оплату',
label_tx_hash: 'Transaction ID (TX Hash)',
placeholder_tx_hash: '64 шестнадцатеричных символа...',
label_tx_key: 'Transaction Key (TX Key)',
placeholder_tx_key: '64 шестнадцатеричных символа...',
btn_verify_proof: 'Проверить оплату',
proof_verifying: 'Проверка...',
proof_verified: 'Оплата подтверждена: {amount} XMR',
proof_no_match: 'Соответствующий выход не найден — неверный TX Key или адрес',
proof_tx_not_found: 'Транзакция не найдена',
proof_error: 'Ошибка проверки',
status_paid: 'Оплачено'
} }
}; };
var currentLang = 'de'; var currentLang = 'en';
function detectLang() { function detectLang() {
var saved = null; var saved = null;
try { saved = localStorage.getItem('xmrpay_lang'); } catch (e) {} try { saved = localStorage.getItem('xmrpay_lang'); } catch (e) {}
if (saved && translations[saved]) return saved; if (saved && translations[saved]) return saved;
var navLangs = navigator.languages || [navigator.language || 'de']; var navLangs = navigator.languages || [navigator.language || 'en'];
for (var i = 0; i < navLangs.length; i++) { for (var i = 0; i < navLangs.length; i++) {
var code = navLangs[i].substring(0, 2).toLowerCase(); var code = navLangs[i].substring(0, 2).toLowerCase();
if (translations[code]) return code; if (translations[code]) return code;
@@ -143,7 +378,7 @@ var I18n = (function () {
// Update toggle label // Update toggle label
var cur = document.getElementById('langCurrent'); var cur = document.getElementById('langCurrent');
if (cur) cur.textContent = languages[lang].flag; if (cur) cur.textContent = languages[lang].name;
// Update dropdown active state // Update dropdown active state
document.querySelectorAll('.lang-option').forEach(function (btn) { document.querySelectorAll('.lang-option').forEach(function (btn) {

2
i18n.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -112,7 +112,7 @@
<path d="M2 12h20"/> <path d="M2 12h20"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/> <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/>
</svg> </svg>
<span id="langCurrent">DE</span> <span id="langCurrent">English</span>
</button> </button>
<div class="lang-dropdown" id="langDropdown"></div> <div class="lang-dropdown" id="langDropdown"></div>
</div> </div>

View File

@@ -7,6 +7,15 @@
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('fonts/inter-cyrillic.woff2') format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face { @font-face {
font-family: 'JetBrains Mono'; font-family: 'JetBrains Mono';
font-style: normal; font-style: normal;