diff --git a/CLAUDE.md b/CLAUDE.md index a70bbb8..6c5e4dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -210,12 +210,13 @@ Two static SPAs served directly by FastAPI (`main.py` mounts `app/static/` and a Both talk to the same JSON API; there's no admin/user API split beyond `require_admin`. -## Internationalization (`/` only) +## Internationalization (`/` and `/report-bug`) -`app/static/i18n.js` holds every user-facing string of `/` in 7 languages (en, it, es, fr, de, ru, zh) as one flat `TRANSLATIONS` table — no build step, no fetch, loaded before `app.js` so `t()` is always available. Language: `localStorage.plm_lang` → `navigator.language` → `en`. The switcher sits in the **chain-bar, not the navbar**, deliberately: the navbar is hidden until login, which would leave the landing page and login form untranslatable for exactly the users who need it. +`app/static/i18n.js` holds every user-facing string of `/` and `/report-bug` in 7 languages (en, it, es, fr, de, ru, zh) as one flat `TRANSLATIONS` table — no build step, no fetch. `/` loads it before `app.js`; `/report-bug` loads it before its own inline script — either way `t()` is always available by the time it's called. Language: `localStorage.plm_lang` → `navigator.language` → `en`, shared across both pages since they read/write the same `localStorage` key. On `/` the switcher sits in the **chain-bar, not the navbar**, deliberately: the navbar is hidden until login, which would leave the landing page and login form untranslatable for exactly the users who need it. `/report-bug` has no navbar at all, so its switcher is just a top-right bar of its own. -- Static markup is translated by attribute (`data-i18n`, plus `-html`, `-placeholder`, `-title`, `-aria-label`, `-alt`) via `applyStaticTranslations(root?)`; anything rendered from server data uses `t()` in `app.js` and is re-rendered by `onLanguageChange()`. An element belongs to one camp or the other, **never both**, or the two mechanisms overwrite each other — that's why `#bet-btn` has no `data-i18n`: its label carries the configurable bet amount, so `renderBetButton()` owns it. +- Static markup is translated by attribute (`data-i18n`, plus `-html`, `-placeholder`, `-title`, `-aria-label`, `-alt`) via `applyStaticTranslations(root?)`; anything rendered from server data uses `t()` directly (`app.js`'s `onLanguageChange()`, `report-bug.html`'s own inline equivalent) and is re-rendered on a language switch. An element belongs to one camp or the other, **never both**, or the two mechanisms overwrite each other — that's why `#bet-btn` has no `data-i18n`: its label carries the configurable bet amount, so `renderBetButton()` owns it. - **Every language must have exactly the same key set.** There is no fallback beyond `en`; a missing key renders as the raw key string. +- `/report-bug`'s `bugReport.englishNotice` string is itself translated into all 7 languages — it just always *says*, in whichever language the visitor reads, to write the actual bug description in English (so the admin panel, which is Italian-operator-facing and untranslated, doesn't end up with reports in 7 different languages). - `/admin` is intentionally untranslated (operator-facing, Italian), as is `/guida`. **API error contract** (`app/api/errors.py`) — the API is single-language by design. Failures answer with a structured `detail`: `{"code", "message", "params"}`, where `message` is English for non-dashboard consumers and `code` is what the frontend maps to `error.` in `i18n.js` (falling back to `message` for an unknown code). `BetError`/`WithdrawalError` subclass `ApiError` and carry the code from where the failure happens. Even the catch-all 500 handler answers in that shape (`internal_error`), so clients never special-case unexpected errors, and the exception text stays in `logs/app.log`. Adding a user-facing error: give it a code, add `error.` to all 7 languages, and pass values through `params` (amounts as `*_sats` — the frontend derives a `*_plm` sibling) instead of baking them into English text. diff --git a/app/static/i18n.js b/app/static/i18n.js index 38fa014..0e95c11 100644 --- a/app/static/i18n.js +++ b/app/static/i18n.js @@ -10,6 +10,26 @@ const TRANSLATIONS = { 'nav.guideTitle': 'Guide', 'nav.guideAria': 'Open the user guide', 'nav.bugReport': 'Report a bug', + 'bugReport.pageTitle': 'Report a bug', + 'bugReport.heading': 'Report a bug', + 'bugReport.intro': 'Found a problem? Describe it below — your report goes straight to the admin panel.', + 'bugReport.englishNotice': "Please write your bug report in English, regardless of the language you're browsing in — this helps us handle it faster.", + 'bugReport.descriptionLabel': 'What happened?', + 'bugReport.descriptionPlaceholder': 'Describe the bug: what you were doing, what you expected, and what happened instead.', + 'bugReport.contactLabel': 'Contact (optional)', + 'bugReport.contactPlaceholder': "Email or other contact, if you'd like a reply", + 'bugReport.submitBtn': 'Send report', + 'bugReport.submitting': 'Sending…', + 'bugReport.blankError': 'Describe the bug before sending.', + 'bugReport.successToast': 'Thanks! Report sent.', + 'bugReport.errorPrefix': 'Error sending: ', + 'bugReport.myReportsTitle': 'Your reports', + 'bugReport.myReportsHint': 'Only reports sent from this account, with the status set by the admin team.', + 'bugReport.myReportsEmpty': "You haven't sent any reports yet.", + 'bugReport.statusOpen': 'Not read yet', + 'bugReport.statusRead': 'Read', + 'bugReport.statusResolved': 'Resolved', + 'bugReport.backLink': '← Back to home', 'nav.logoutTitle': 'Log out', 'nav.logoutAria': 'Log out of your account', 'nav.deposit': 'Deposit', @@ -153,6 +173,26 @@ const TRANSLATIONS = { 'nav.guideTitle': 'Guida', 'nav.guideAria': 'Apri la guida utente', 'nav.bugReport': 'Segnala un bug', + 'bugReport.pageTitle': 'Segnala un bug', + 'bugReport.heading': 'Segnala un bug', + 'bugReport.intro': 'Hai trovato un problema? Descrivilo qui sotto: la segnalazione arriva direttamente al pannello di amministrazione.', + 'bugReport.englishNotice': "Scrivi la segnalazione in inglese, indipendentemente dalla lingua che stai usando per navigare: questo ci aiuta a gestirla più velocemente.", + 'bugReport.descriptionLabel': 'Cosa è successo?', + 'bugReport.descriptionPlaceholder': 'Descrivi il bug: cosa stavi facendo, cosa ti aspettavi e cosa è successo invece.', + 'bugReport.contactLabel': 'Contatto (opzionale)', + 'bugReport.contactPlaceholder': 'Email o altro recapito, se vuoi essere ricontattato', + 'bugReport.submitBtn': 'Invia segnalazione', + 'bugReport.submitting': 'Invio…', + 'bugReport.blankError': 'Descrivi il bug prima di inviare.', + 'bugReport.successToast': 'Grazie! Segnalazione inviata.', + 'bugReport.errorPrefix': "Errore nell'invio: ", + 'bugReport.myReportsTitle': 'Le tue segnalazioni', + 'bugReport.myReportsHint': "Solo le segnalazioni inviate da questo account, con lo stato aggiornato dall'amministrazione.", + 'bugReport.myReportsEmpty': 'Non hai ancora inviato segnalazioni.', + 'bugReport.statusOpen': 'Da leggere', + 'bugReport.statusRead': 'Letta', + 'bugReport.statusResolved': 'Risolta', + 'bugReport.backLink': '← Torna alla home', 'nav.logoutTitle': 'Esci', 'nav.logoutAria': "Esci dall'account", 'nav.deposit': 'Deposito', @@ -293,6 +333,26 @@ const TRANSLATIONS = { 'nav.guideTitle': 'Guía', 'nav.guideAria': 'Abrir la guía del usuario', 'nav.bugReport': 'Reportar un error', + 'bugReport.pageTitle': 'Reportar un error', + 'bugReport.heading': 'Reportar un error', + 'bugReport.intro': '¿Encontraste un problema? Descríbelo a continuación: el informe llega directamente al panel de administración.', + 'bugReport.englishNotice': 'Escribe el informe en inglés, independientemente del idioma que estés usando para navegar: esto nos ayuda a gestionarlo más rápido.', + 'bugReport.descriptionLabel': '¿Qué pasó?', + 'bugReport.descriptionPlaceholder': 'Describe el error: qué estabas haciendo, qué esperabas y qué sucedió en su lugar.', + 'bugReport.contactLabel': 'Contacto (opcional)', + 'bugReport.contactPlaceholder': 'Correo u otro contacto, si quieres que te respondamos', + 'bugReport.submitBtn': 'Enviar informe', + 'bugReport.submitting': 'Enviando…', + 'bugReport.blankError': 'Describe el error antes de enviarlo.', + 'bugReport.successToast': '¡Gracias! Informe enviado.', + 'bugReport.errorPrefix': 'Error al enviar: ', + 'bugReport.myReportsTitle': 'Tus informes', + 'bugReport.myReportsHint': 'Solo los informes enviados desde esta cuenta, con el estado actualizado por el equipo de administración.', + 'bugReport.myReportsEmpty': 'Todavía no has enviado ningún informe.', + 'bugReport.statusOpen': 'Sin leer', + 'bugReport.statusRead': 'Leído', + 'bugReport.statusResolved': 'Resuelto', + 'bugReport.backLink': '← Volver al inicio', 'nav.logoutTitle': 'Salir', 'nav.logoutAria': 'Cerrar sesión', 'nav.deposit': 'Depósito', @@ -433,6 +493,26 @@ const TRANSLATIONS = { 'nav.guideTitle': 'Guide', 'nav.guideAria': "Ouvrir le guide de l'utilisateur", 'nav.bugReport': 'Signaler un bug', + 'bugReport.pageTitle': 'Signaler un bug', + 'bugReport.heading': 'Signaler un bug', + 'bugReport.intro': "Vous avez trouvé un problème ? Décrivez-le ci-dessous : le signalement arrive directement dans le panneau d'administration.", + 'bugReport.englishNotice': "Rédigez votre signalement en anglais, quelle que soit la langue que vous utilisez pour naviguer : cela nous aide à le traiter plus rapidement.", + 'bugReport.descriptionLabel': "Que s'est-il passé ?", + 'bugReport.descriptionPlaceholder': "Décrivez le bug : ce que vous faisiez, ce que vous attendiez et ce qui s'est passé à la place.", + 'bugReport.contactLabel': 'Contact (facultatif)', + 'bugReport.contactPlaceholder': 'Email ou autre contact, si vous souhaitez une réponse', + 'bugReport.submitBtn': 'Envoyer le signalement', + 'bugReport.submitting': 'Envoi…', + 'bugReport.blankError': "Décrivez le bug avant d'envoyer.", + 'bugReport.successToast': 'Merci ! Signalement envoyé.', + 'bugReport.errorPrefix': "Erreur lors de l'envoi : ", + 'bugReport.myReportsTitle': 'Vos signalements', + 'bugReport.myReportsHint': "Seulement les signalements envoyés depuis ce compte, avec le statut mis à jour par l'équipe d'administration.", + 'bugReport.myReportsEmpty': "Vous n'avez encore envoyé aucun signalement.", + 'bugReport.statusOpen': 'Non lu', + 'bugReport.statusRead': 'Lu', + 'bugReport.statusResolved': 'Résolu', + 'bugReport.backLink': "← Retour à l'accueil", 'nav.logoutTitle': 'Se déconnecter', 'nav.logoutAria': 'Se déconnecter du compte', 'nav.deposit': 'Dépôt', @@ -573,6 +653,26 @@ const TRANSLATIONS = { 'nav.guideTitle': 'Anleitung', 'nav.guideAria': 'Benutzerhandbuch öffnen', 'nav.bugReport': 'Fehler melden', + 'bugReport.pageTitle': 'Fehler melden', + 'bugReport.heading': 'Fehler melden', + 'bugReport.intro': 'Ein Problem gefunden? Beschreibe es unten — die Meldung geht direkt an das Admin-Panel.', + 'bugReport.englishNotice': 'Bitte schreibe die Fehlermeldung auf Englisch, unabhängig von der Sprache, die du gerade verwendest — das hilft uns, sie schneller zu bearbeiten.', + 'bugReport.descriptionLabel': 'Was ist passiert?', + 'bugReport.descriptionPlaceholder': 'Beschreibe den Fehler: was du getan hast, was du erwartet hast und was stattdessen passiert ist.', + 'bugReport.contactLabel': 'Kontakt (optional)', + 'bugReport.contactPlaceholder': 'E-Mail oder anderer Kontakt, falls du eine Antwort möchtest', + 'bugReport.submitBtn': 'Meldung senden', + 'bugReport.submitting': 'Senden…', + 'bugReport.blankError': 'Beschreibe den Fehler, bevor du sendest.', + 'bugReport.successToast': 'Danke! Meldung gesendet.', + 'bugReport.errorPrefix': 'Fehler beim Senden: ', + 'bugReport.myReportsTitle': 'Deine Meldungen', + 'bugReport.myReportsHint': 'Nur Meldungen, die von diesem Konto gesendet wurden, mit dem vom Admin-Team aktualisierten Status.', + 'bugReport.myReportsEmpty': 'Du hast noch keine Meldungen gesendet.', + 'bugReport.statusOpen': 'Ungelesen', + 'bugReport.statusRead': 'Gelesen', + 'bugReport.statusResolved': 'Gelöst', + 'bugReport.backLink': '← Zurück zur Startseite', 'nav.logoutTitle': 'Abmelden', 'nav.logoutAria': 'Vom Konto abmelden', 'nav.deposit': 'Einzahlung', @@ -713,6 +813,26 @@ const TRANSLATIONS = { 'nav.guideTitle': 'Инструкция', 'nav.guideAria': 'Открыть руководство пользователя', 'nav.bugReport': 'Сообщить об ошибке', + 'bugReport.pageTitle': 'Сообщить об ошибке', + 'bugReport.heading': 'Сообщить об ошибке', + 'bugReport.intro': 'Нашли проблему? Опишите её ниже — сообщение сразу попадёт в панель администратора.', + 'bugReport.englishNotice': 'Пожалуйста, опишите ошибку на английском языке, независимо от языка интерфейса — это поможет нам обработать её быстрее.', + 'bugReport.descriptionLabel': 'Что произошло?', + 'bugReport.descriptionPlaceholder': 'Опишите ошибку: что вы делали, что ожидали и что произошло вместо этого.', + 'bugReport.contactLabel': 'Контакт (необязательно)', + 'bugReport.contactPlaceholder': 'Email или другой контакт, если хотите получить ответ', + 'bugReport.submitBtn': 'Отправить сообщение', + 'bugReport.submitting': 'Отправка…', + 'bugReport.blankError': 'Опишите ошибку перед отправкой.', + 'bugReport.successToast': 'Спасибо! Сообщение отправлено.', + 'bugReport.errorPrefix': 'Ошибка отправки: ', + 'bugReport.myReportsTitle': 'Ваши сообщения', + 'bugReport.myReportsHint': 'Только сообщения, отправленные с этого аккаунта, со статусом, обновлённым администрацией.', + 'bugReport.myReportsEmpty': 'Вы ещё не отправляли сообщений.', + 'bugReport.statusOpen': 'Не прочитано', + 'bugReport.statusRead': 'Прочитано', + 'bugReport.statusResolved': 'Решено', + 'bugReport.backLink': '← Назад на главную', 'nav.logoutTitle': 'Выйти', 'nav.logoutAria': 'Выйти из аккаунта', 'nav.deposit': 'Депозит', @@ -853,6 +973,26 @@ const TRANSLATIONS = { 'nav.guideTitle': '指南', 'nav.guideAria': '打开用户指南', 'nav.bugReport': '报告问题', + 'bugReport.pageTitle': '报告问题', + 'bugReport.heading': '报告问题', + 'bugReport.intro': '发现问题了吗?请在下面描述——您的反馈会直接发送到管理员面板。', + 'bugReport.englishNotice': '请用英文描述问题,无论您当前使用的是哪种语言界面——这有助于我们更快处理。', + 'bugReport.descriptionLabel': '发生了什么?', + 'bugReport.descriptionPlaceholder': '描述问题:您当时在做什么、期望的结果是什么,以及实际发生了什么。', + 'bugReport.contactLabel': '联系方式(可选)', + 'bugReport.contactPlaceholder': '如果希望得到回复,请留下邮箱或其他联系方式', + 'bugReport.submitBtn': '发送反馈', + 'bugReport.submitting': '发送中…', + 'bugReport.blankError': '请先描述问题再发送。', + 'bugReport.successToast': '谢谢!反馈已发送。', + 'bugReport.errorPrefix': '发送出错:', + 'bugReport.myReportsTitle': '您的反馈', + 'bugReport.myReportsHint': '仅显示此账户发送的反馈,状态由管理团队更新。', + 'bugReport.myReportsEmpty': '您还没有发送过反馈。', + 'bugReport.statusOpen': '待处理', + 'bugReport.statusRead': '已读', + 'bugReport.statusResolved': '已解决', + 'bugReport.backLink': '← 返回首页', 'nav.logoutTitle': '退出登录', 'nav.logoutAria': '退出账户', 'nav.deposit': '存款', diff --git a/app/static/report-bug.html b/app/static/report-bug.html index 3beb1b9..38b96e7 100644 --- a/app/static/report-bug.html +++ b/app/static/report-bug.html @@ -1,37 +1,53 @@ - + - Segnala un bug — PLM Lottery + Report a bug
-

Segnala un bug

-

Hai trovato un problema? Descrivilo qui sotto: la segnalazione arriva direttamente al pannello di amministrazione.

+
+ +
+ +

Report a bug

+

Found a problem? Describe it below — your report goes straight to the admin panel.

+

+ Please write your bug report in English, regardless of the language you're browsing in — this helps us handle it faster. +

- - + + - - + + - +
-

← Torna alla home

+

← Back to home

+ diff --git a/app/static/style.css b/app/static/style.css index 5275e53..0a7f111 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -254,6 +254,13 @@ button.link:hover, a.link:hover { filter: none; color: var(--color-foreground); .toast.error { background: var(--color-destructive-bg); color: var(--color-destructive); } @keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } +.lang-bar { display: flex; justify-content: flex-end; margin-bottom: 4px; } + +.english-notice { + background: #FEF3C7; color: #92400E; border: 1px solid #F59E0B; + border-radius: var(--radius-sm); padding: 10px 12px; font-size: 0.85rem; font-weight: 500; +} + .badge { display: inline-block; font-size: 0.72rem; font-weight: 600; padding: 2px 8px; border-radius: 999px; background: var(--color-background); border: 1px solid var(--color-border);