Browse Source

Explain a blocked camera in the barcode scanner, offer it again, and take a typed code on the first press (#489)

* Explain a blocked camera in the barcode scanner, offer it again, and take a typed code on the first press

A refused camera now shows why, in the workshop's language, with a Try
again button, and the camera starts by itself once the browser reports it
allowed. Typing the code is offered right away and is taken on the first
press; it used to go through the camera's three-reads check, so Go had to be
pressed three times. A refusal is no longer followed by a second request for
any camera, and an HTTP page says so without asking for the camera at all.

* Restart the camera when it is already allowed by the time the browser answers, once per opening
Bernt Christian Egeland 1 ngày trước cách đây
mục cha
commit
23fbf2b402

+ 20 - 0
messages/de/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Manuell eingeben",
     "enterManually": "Manuell eingeben",
     "enterBarcode": "Barcode eingeben…",
     "enterBarcode": "Barcode eingeben…",
     "scanHint": "Richten Sie die Kamera auf einen Barcode oder geben Sie ihn manuell ein",
     "scanHint": "Richten Sie die Kamera auf einen Barcode oder geben Sie ihn manuell ein",
+    "submitBarcode": "Los",
+    "camera": {
+      "tryAgain": "Erneut versuchen",
+      "denied": {
+        "title": "Kamerazugriff ist blockiert",
+        "body": "Erlauben Sie die Kamera für diese Seite in Ihrem Browser, über das Kamera- oder Schlosssymbol in der Adressleiste, und klicken Sie dann auf Erneut versuchen. Oder geben Sie den Code unten ein."
+      },
+      "insecure": {
+        "title": "Die Kamera braucht HTTPS",
+        "body": "Browser erlauben die Kamera nur über eine sichere Verbindung, und diese Seite läuft über HTTP ({host}). Öffnen Sie Torqvoice über HTTPS, um zu scannen, oder geben Sie den Code unten ein."
+      },
+      "notFound": {
+        "title": "Keine Kamera gefunden",
+        "body": "Schließen Sie eine Kamera an und klicken Sie auf Erneut versuchen, oder geben Sie den Code unten ein."
+      },
+      "failed": {
+        "title": "Die Kamera konnte nicht starten",
+        "body": "Vielleicht verwendet eine andere App sie. Schließen Sie diese App und klicken Sie auf Erneut versuchen, oder geben Sie den Code unten ein."
+      }
+    },
     "iosInstallHint": "Auf Teilen > Zum Home-Bildschirm tippen",
     "iosInstallHint": "Auf Teilen > Zum Home-Bildschirm tippen",
     "installDescription": "Zum Home-Bildschirm hinzufügen für schnellen Zugriff",
     "installDescription": "Zum Home-Bildschirm hinzufügen für schnellen Zugriff",
     "enterFullscreen": "Vollbild",
     "enterFullscreen": "Vollbild",

+ 20 - 0
messages/en/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Enter manually",
     "enterManually": "Enter manually",
     "enterBarcode": "Enter barcode...",
     "enterBarcode": "Enter barcode...",
     "scanHint": "Point your camera at a barcode or enter it manually",
     "scanHint": "Point your camera at a barcode or enter it manually",
+    "submitBarcode": "Go",
+    "camera": {
+      "tryAgain": "Try again",
+      "denied": {
+        "title": "Camera access is blocked",
+        "body": "Allow the camera for this site in your browser, from the camera or lock icon in the address bar, then press Try again. Or type the code below."
+      },
+      "insecure": {
+        "title": "The camera needs HTTPS",
+        "body": "Browsers only allow the camera on a secure connection, and this page is on HTTP ({host}). Open Torqvoice over HTTPS to scan, or type the code below."
+      },
+      "notFound": {
+        "title": "No camera found",
+        "body": "Connect a camera and press Try again, or type the code below."
+      },
+      "failed": {
+        "title": "The camera could not start",
+        "body": "Another app may be using it. Close that app and press Try again, or type the code below."
+      }
+    },
     "iosInstallHint": "Tap Share > Add to Home Screen",
     "iosInstallHint": "Tap Share > Add to Home Screen",
     "installDescription": "Add to your home screen for quick access",
     "installDescription": "Add to your home screen for quick access",
     "enterFullscreen": "Fullscreen",
     "enterFullscreen": "Fullscreen",

+ 20 - 0
messages/es/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Introducir manualmente",
     "enterManually": "Introducir manualmente",
     "enterBarcode": "Introducir código de barras…",
     "enterBarcode": "Introducir código de barras…",
     "scanHint": "Apunte la cámara a un código de barras o introdúzcalo manualmente",
     "scanHint": "Apunte la cámara a un código de barras o introdúzcalo manualmente",
+    "submitBarcode": "Ir",
+    "camera": {
+      "tryAgain": "Reintentar",
+      "denied": {
+        "title": "El acceso a la cámara está bloqueado",
+        "body": "Permita la cámara para este sitio en su navegador, desde el icono de la cámara o del candado en la barra de direcciones, y pulse Reintentar. O escriba el código abajo."
+      },
+      "insecure": {
+        "title": "La cámara necesita HTTPS",
+        "body": "Los navegadores solo permiten la cámara con una conexión segura, y esta página está en HTTP ({host}). Abra Torqvoice por HTTPS para escanear, o escriba el código abajo."
+      },
+      "notFound": {
+        "title": "No se encontró ninguna cámara",
+        "body": "Conecte una cámara y pulse Reintentar, o escriba el código abajo."
+      },
+      "failed": {
+        "title": "No se pudo iniciar la cámara",
+        "body": "Puede que otra aplicación la esté usando. Ciérrela y pulse Reintentar, o escriba el código abajo."
+      }
+    },
     "iosInstallHint": "Toque Compartir > Añadir a inicio",
     "iosInstallHint": "Toque Compartir > Añadir a inicio",
     "installDescription": "Añádela a tu pantalla de inicio para acceder rápido",
     "installDescription": "Añádela a tu pantalla de inicio para acceder rápido",
     "enterFullscreen": "Pantalla completa",
     "enterFullscreen": "Pantalla completa",

+ 20 - 0
messages/fr/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Saisir manuellement",
     "enterManually": "Saisir manuellement",
     "enterBarcode": "Saisir un code-barres…",
     "enterBarcode": "Saisir un code-barres…",
     "scanHint": "Pointez la caméra vers un code-barres ou saisissez-le manuellement",
     "scanHint": "Pointez la caméra vers un code-barres ou saisissez-le manuellement",
+    "submitBarcode": "OK",
+    "camera": {
+      "tryAgain": "Réessayer",
+      "denied": {
+        "title": "L'accès à la caméra est bloqué",
+        "body": "Autorisez la caméra pour ce site dans votre navigateur, depuis l'icône de caméra ou de cadenas dans la barre d'adresse, puis cliquez sur Réessayer. Ou saisissez le code ci-dessous."
+      },
+      "insecure": {
+        "title": "La caméra nécessite HTTPS",
+        "body": "Les navigateurs n'autorisent la caméra que sur une connexion sécurisée, et cette page est en HTTP ({host}). Ouvrez Torqvoice en HTTPS pour scanner, ou saisissez le code ci-dessous."
+      },
+      "notFound": {
+        "title": "Aucune caméra trouvée",
+        "body": "Branchez une caméra et cliquez sur Réessayer, ou saisissez le code ci-dessous."
+      },
+      "failed": {
+        "title": "La caméra n'a pas pu démarrer",
+        "body": "Une autre application l'utilise peut-être. Fermez-la et cliquez sur Réessayer, ou saisissez le code ci-dessous."
+      }
+    },
     "iosInstallHint": "Touchez Partager > Sur l'écran d'accueil",
     "iosInstallHint": "Touchez Partager > Sur l'écran d'accueil",
     "installDescription": "Ajoutez-la à votre écran d'accueil pour un accès rapide",
     "installDescription": "Ajoutez-la à votre écran d'accueil pour un accès rapide",
     "enterFullscreen": "Plein écran",
     "enterFullscreen": "Plein écran",

+ 20 - 0
messages/it/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Inserisci manualmente",
     "enterManually": "Inserisci manualmente",
     "enterBarcode": "Inserisci codice a barre…",
     "enterBarcode": "Inserisci codice a barre…",
     "scanHint": "Punta la fotocamera su un codice a barre oppure inseriscilo manualmente",
     "scanHint": "Punta la fotocamera su un codice a barre oppure inseriscilo manualmente",
+    "submitBarcode": "Vai",
+    "camera": {
+      "tryAgain": "Riprova",
+      "denied": {
+        "title": "L'accesso alla fotocamera è bloccato",
+        "body": "Consenti la fotocamera per questo sito nel browser, dall'icona della fotocamera o del lucchetto nella barra degli indirizzi, poi premi Riprova. Oppure digita il codice qui sotto."
+      },
+      "insecure": {
+        "title": "La fotocamera richiede HTTPS",
+        "body": "I browser consentono la fotocamera solo su una connessione sicura, e questa pagina è in HTTP ({host}). Apri Torqvoice in HTTPS per scansionare, oppure digita il codice qui sotto."
+      },
+      "notFound": {
+        "title": "Nessuna fotocamera trovata",
+        "body": "Collega una fotocamera e premi Riprova, oppure digita il codice qui sotto."
+      },
+      "failed": {
+        "title": "Impossibile avviare la fotocamera",
+        "body": "Forse un'altra app la sta usando. Chiudila e premi Riprova, oppure digita il codice qui sotto."
+      }
+    },
     "iosInstallHint": "Tocca Condividi > Aggiungi a Home",
     "iosInstallHint": "Tocca Condividi > Aggiungi a Home",
     "installDescription": "Aggiungila alla schermata Home per un accesso rapido",
     "installDescription": "Aggiungila alla schermata Home per un accesso rapido",
     "enterFullscreen": "Schermo intero",
     "enterFullscreen": "Schermo intero",

+ 20 - 0
messages/lt/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Įvesti ranka",
     "enterManually": "Įvesti ranka",
     "enterBarcode": "Įveskite brūkšninį kodą…",
     "enterBarcode": "Įveskite brūkšninį kodą…",
     "scanHint": "Nukreipkite kamerą į brūkšninį kodą arba įveskite jį ranka",
     "scanHint": "Nukreipkite kamerą į brūkšninį kodą arba įveskite jį ranka",
+    "submitBarcode": "Gerai",
+    "camera": {
+      "tryAgain": "Bandyti dar kartą",
+      "denied": {
+        "title": "Prieiga prie kameros užblokuota",
+        "body": "Leiskite naudoti kamerą šiai svetainei naršyklėje, paspaudę kameros arba spynos piktogramą adreso juostoje, tada spauskite Bandyti dar kartą. Arba įveskite kodą žemiau."
+      },
+      "insecure": {
+        "title": "Kamerai reikia HTTPS",
+        "body": "Naršyklės leidžia naudoti kamerą tik saugiu ryšiu, o šis puslapis atidarytas per HTTP ({host}). Atidarykite Torqvoice per HTTPS, kad galėtumėte skenuoti, arba įveskite kodą žemiau."
+      },
+      "notFound": {
+        "title": "Kamera nerasta",
+        "body": "Prijunkite kamerą ir spauskite Bandyti dar kartą arba įveskite kodą žemiau."
+      },
+      "failed": {
+        "title": "Nepavyko įjungti kameros",
+        "body": "Galbūt ją naudoja kita programa. Uždarykite ją ir spauskite Bandyti dar kartą arba įveskite kodą žemiau."
+      }
+    },
     "iosInstallHint": "Palieskite Bendrinti > Įtraukti į pradžios ekraną",
     "iosInstallHint": "Palieskite Bendrinti > Įtraukti į pradžios ekraną",
     "installDescription": "Įtraukite į pradžios ekraną, kad pasiektumėte greičiau",
     "installDescription": "Įtraukite į pradžios ekraną, kad pasiektumėte greičiau",
     "enterFullscreen": "Visas ekranas",
     "enterFullscreen": "Visas ekranas",

+ 20 - 0
messages/nb/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Skriv inn manuelt",
     "enterManually": "Skriv inn manuelt",
     "enterBarcode": "Skriv inn strekkode…",
     "enterBarcode": "Skriv inn strekkode…",
     "scanHint": "Rett kameraet mot en strekkode, eller skriv den inn manuelt",
     "scanHint": "Rett kameraet mot en strekkode, eller skriv den inn manuelt",
+    "submitBarcode": "OK",
+    "camera": {
+      "tryAgain": "Prøv igjen",
+      "denied": {
+        "title": "Tilgang til kameraet er blokkert",
+        "body": "Tillat kameraet for dette nettstedet i nettleseren, fra kamera- eller hengelåsikonet i adressefeltet, og trykk deretter Prøv igjen. Eller skriv inn koden nedenfor."
+      },
+      "insecure": {
+        "title": "Kameraet krever HTTPS",
+        "body": "Nettlesere tillater bare kameraet over en sikker tilkobling, og denne siden er på HTTP ({host}). Åpne Torqvoice over HTTPS for å skanne, eller skriv inn koden nedenfor."
+      },
+      "notFound": {
+        "title": "Fant ikke noe kamera",
+        "body": "Koble til et kamera og trykk Prøv igjen, eller skriv inn koden nedenfor."
+      },
+      "failed": {
+        "title": "Kameraet kunne ikke starte",
+        "body": "Kanskje en annen app bruker det. Lukk den appen og trykk Prøv igjen, eller skriv inn koden nedenfor."
+      }
+    },
     "iosInstallHint": "Trykk Del > Legg til på Hjem-skjerm",
     "iosInstallHint": "Trykk Del > Legg til på Hjem-skjerm",
     "installDescription": "Legg den til på hjem-skjermen for rask tilgang",
     "installDescription": "Legg den til på hjem-skjermen for rask tilgang",
     "enterFullscreen": "Fullskjerm",
     "enterFullscreen": "Fullskjerm",

+ 20 - 0
messages/nl/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Handmatig invoeren",
     "enterManually": "Handmatig invoeren",
     "enterBarcode": "Barcode invoeren…",
     "enterBarcode": "Barcode invoeren…",
     "scanHint": "Richt uw camera op een barcode of voer hem handmatig in",
     "scanHint": "Richt uw camera op een barcode of voer hem handmatig in",
+    "submitBarcode": "Ga",
+    "camera": {
+      "tryAgain": "Opnieuw proberen",
+      "denied": {
+        "title": "Toegang tot de camera is geblokkeerd",
+        "body": "Sta de camera toe voor deze site in uw browser, via het camera- of slotpictogram in de adresbalk, en klik daarna op Opnieuw proberen. Of typ de code hieronder."
+      },
+      "insecure": {
+        "title": "De camera heeft HTTPS nodig",
+        "body": "Browsers staan de camera alleen toe via een beveiligde verbinding, en deze pagina draait op HTTP ({host}). Open Torqvoice via HTTPS om te scannen, of typ de code hieronder."
+      },
+      "notFound": {
+        "title": "Geen camera gevonden",
+        "body": "Sluit een camera aan en klik op Opnieuw proberen, of typ de code hieronder."
+      },
+      "failed": {
+        "title": "De camera kon niet starten",
+        "body": "Misschien gebruikt een andere app hem. Sluit die app en klik op Opnieuw proberen, of typ de code hieronder."
+      }
+    },
     "iosInstallHint": "Tik op Deel > Zet op beginscherm",
     "iosInstallHint": "Tik op Deel > Zet op beginscherm",
     "installDescription": "Zet de app op je beginscherm voor snelle toegang",
     "installDescription": "Zet de app op je beginscherm voor snelle toegang",
     "enterFullscreen": "Volledig scherm",
     "enterFullscreen": "Volledig scherm",

+ 20 - 0
messages/pl/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Wprowadź ręcznie",
     "enterManually": "Wprowadź ręcznie",
     "enterBarcode": "Wprowadź kod kreskowy…",
     "enterBarcode": "Wprowadź kod kreskowy…",
     "scanHint": "Skieruj aparat na kod kreskowy albo wprowadź go ręcznie",
     "scanHint": "Skieruj aparat na kod kreskowy albo wprowadź go ręcznie",
+    "submitBarcode": "OK",
+    "camera": {
+      "tryAgain": "Spróbuj ponownie",
+      "denied": {
+        "title": "Dostęp do aparatu jest zablokowany",
+        "body": "Zezwól tej stronie na użycie aparatu w przeglądarce, ikoną aparatu lub kłódki na pasku adresu, a potem kliknij Spróbuj ponownie. Albo wpisz kod poniżej."
+      },
+      "insecure": {
+        "title": "Aparat wymaga HTTPS",
+        "body": "Przeglądarki pozwalają używać aparatu tylko przy bezpiecznym połączeniu, a ta strona działa przez HTTP ({host}). Otwórz Torqvoice przez HTTPS, żeby skanować, albo wpisz kod poniżej."
+      },
+      "notFound": {
+        "title": "Nie znaleziono aparatu",
+        "body": "Podłącz aparat i kliknij Spróbuj ponownie albo wpisz kod poniżej."
+      },
+      "failed": {
+        "title": "Nie udało się uruchomić aparatu",
+        "body": "Być może używa go inna aplikacja. Zamknij ją i kliknij Spróbuj ponownie albo wpisz kod poniżej."
+      }
+    },
     "iosInstallHint": "Dotknij Udostępnij > Do ekranu początkowego",
     "iosInstallHint": "Dotknij Udostępnij > Do ekranu początkowego",
     "installDescription": "Dodaj do ekranu głównego, aby mieć szybki dostęp",
     "installDescription": "Dodaj do ekranu głównego, aby mieć szybki dostęp",
     "enterFullscreen": "Pełny ekran",
     "enterFullscreen": "Pełny ekran",

+ 20 - 0
messages/pt-BR/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Digitar manualmente",
     "enterManually": "Digitar manualmente",
     "enterBarcode": "Digitar código de barras…",
     "enterBarcode": "Digitar código de barras…",
     "scanHint": "Aponte a câmera para um código de barras ou digite-o manualmente",
     "scanHint": "Aponte a câmera para um código de barras ou digite-o manualmente",
+    "submitBarcode": "Ir",
+    "camera": {
+      "tryAgain": "Tentar de novo",
+      "denied": {
+        "title": "O acesso à câmera está bloqueado",
+        "body": "Permita a câmera para este site no navegador, pelo ícone de câmera ou de cadeado na barra de endereço, e toque em Tentar de novo. Ou digite o código abaixo."
+      },
+      "insecure": {
+        "title": "A câmera precisa de HTTPS",
+        "body": "Os navegadores só liberam a câmera em uma conexão segura, e esta página está em HTTP ({host}). Abra o Torqvoice por HTTPS para escanear, ou digite o código abaixo."
+      },
+      "notFound": {
+        "title": "Nenhuma câmera encontrada",
+        "body": "Conecte uma câmera e toque em Tentar de novo, ou digite o código abaixo."
+      },
+      "failed": {
+        "title": "Não foi possível iniciar a câmera",
+        "body": "Talvez outro app esteja usando a câmera. Feche esse app e toque em Tentar de novo, ou digite o código abaixo."
+      }
+    },
     "iosInstallHint": "Toque em Compartilhar > Adicionar à Tela de Início",
     "iosInstallHint": "Toque em Compartilhar > Adicionar à Tela de Início",
     "installDescription": "Adicione à tela de início para acesso rápido",
     "installDescription": "Adicione à tela de início para acesso rápido",
     "enterFullscreen": "Tela cheia",
     "enterFullscreen": "Tela cheia",

+ 20 - 0
messages/pt-PT/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Introduzir manualmente",
     "enterManually": "Introduzir manualmente",
     "enterBarcode": "Introduzir código de barras…",
     "enterBarcode": "Introduzir código de barras…",
     "scanHint": "Aponte a câmara para um código de barras ou introduza-o manualmente",
     "scanHint": "Aponte a câmara para um código de barras ou introduza-o manualmente",
+    "submitBarcode": "Ir",
+    "camera": {
+      "tryAgain": "Tentar novamente",
+      "denied": {
+        "title": "O acesso à câmara está bloqueado",
+        "body": "Permita a câmara para este site no navegador, através do ícone da câmara ou do cadeado na barra de endereço, e toque em Tentar novamente. Ou introduza o código abaixo."
+      },
+      "insecure": {
+        "title": "A câmara precisa de HTTPS",
+        "body": "Os navegadores só permitem a câmara numa ligação segura, e esta página está em HTTP ({host}). Abra o Torqvoice por HTTPS para digitalizar, ou introduza o código abaixo."
+      },
+      "notFound": {
+        "title": "Nenhuma câmara encontrada",
+        "body": "Ligue uma câmara e toque em Tentar novamente, ou introduza o código abaixo."
+      },
+      "failed": {
+        "title": "Não foi possível iniciar a câmara",
+        "body": "Talvez outra aplicação a esteja a usar. Feche essa aplicação e toque em Tentar novamente, ou introduza o código abaixo."
+      }
+    },
     "iosInstallHint": "Toque em Partilhar > Adicionar ao ecrã principal",
     "iosInstallHint": "Toque em Partilhar > Adicionar ao ecrã principal",
     "installDescription": "Adicione ao ecrã principal para um acesso rápido",
     "installDescription": "Adicione ao ecrã principal para um acesso rápido",
     "enterFullscreen": "Ecrã inteiro",
     "enterFullscreen": "Ecrã inteiro",

+ 20 - 0
messages/ru/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Ввести вручную",
     "enterManually": "Ввести вручную",
     "enterBarcode": "Введите штрихкод…",
     "enterBarcode": "Введите штрихкод…",
     "scanHint": "Наведите камеру на штрихкод или введите его вручную",
     "scanHint": "Наведите камеру на штрихкод или введите его вручную",
+    "submitBarcode": "OK",
+    "camera": {
+      "tryAgain": "Повторить",
+      "denied": {
+        "title": "Доступ к камере заблокирован",
+        "body": "Разрешите этому сайту доступ к камере в браузере, через значок камеры или замка в адресной строке, а затем нажмите «Повторить». Или введите код ниже."
+      },
+      "insecure": {
+        "title": "Камере нужен HTTPS",
+        "body": "Браузеры дают доступ к камере только по защищённому соединению, а эта страница открыта по HTTP ({host}). Откройте Torqvoice по HTTPS, чтобы сканировать, или введите код ниже."
+      },
+      "notFound": {
+        "title": "Камера не найдена",
+        "body": "Подключите камеру и нажмите «Повторить» или введите код ниже."
+      },
+      "failed": {
+        "title": "Не удалось включить камеру",
+        "body": "Возможно, её использует другое приложение. Закройте его и нажмите «Повторить» или введите код ниже."
+      }
+    },
     "iosInstallHint": "Нажмите «Поделиться» > «На экран «Домой»»",
     "iosInstallHint": "Нажмите «Поделиться» > «На экран «Домой»»",
     "installDescription": "Добавьте на главный экран для быстрого доступа",
     "installDescription": "Добавьте на главный экран для быстрого доступа",
     "enterFullscreen": "Полный экран",
     "enterFullscreen": "Полный экран",

+ 20 - 0
messages/tr/common.json

@@ -76,6 +76,26 @@
     "enterManually": "Elle gir",
     "enterManually": "Elle gir",
     "enterBarcode": "Barkod girin…",
     "enterBarcode": "Barkod girin…",
     "scanHint": "Kamerayı bir barkoda doğrultun ya da elle girin",
     "scanHint": "Kamerayı bir barkoda doğrultun ya da elle girin",
+    "submitBarcode": "Git",
+    "camera": {
+      "tryAgain": "Tekrar dene",
+      "denied": {
+        "title": "Kamera erişimi engellendi",
+        "body": "Tarayıcınızda bu site için kameraya izin verin (adres çubuğundaki kamera ya da kilit simgesinden), ardından Tekrar dene'ye basın. Ya da kodu aşağıya yazın."
+      },
+      "insecure": {
+        "title": "Kamera HTTPS gerektirir",
+        "body": "Tarayıcılar kameraya yalnızca güvenli bağlantıda izin verir ve bu sayfa HTTP ({host}) üzerinde. Taramak için Torqvoice'u HTTPS ile açın ya da kodu aşağıya yazın."
+      },
+      "notFound": {
+        "title": "Kamera bulunamadı",
+        "body": "Bir kamera bağlayıp Tekrar dene'ye basın ya da kodu aşağıya yazın."
+      },
+      "failed": {
+        "title": "Kamera başlatılamadı",
+        "body": "Kamerayı başka bir uygulama kullanıyor olabilir. O uygulamayı kapatıp Tekrar dene'ye basın ya da kodu aşağıya yazın."
+      }
+    },
     "iosInstallHint": "Paylaş > Ana Ekrana Ekle'ye dokunun",
     "iosInstallHint": "Paylaş > Ana Ekrana Ekle'ye dokunun",
     "installDescription": "Hızlı erişim için ana ekranınıza ekleyin",
     "installDescription": "Hızlı erişim için ana ekranınıza ekleyin",
     "enterFullscreen": "Tam ekran",
     "enterFullscreen": "Tam ekran",

+ 161 - 0
src/__tests__/components/barcode-scanner-dialog.test.tsx

@@ -0,0 +1,161 @@
+/**
+ * The barcode scanner when the camera is not there for it.
+ *
+ * People who refuse the camera, or whose browser refuses it for them, fall
+ * back to typing the code. That path has to work on the first press, say
+ * plainly why the camera is missing, and offer the camera again once it is
+ * allowed, without asking the browser a second time after a refusal.
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { act, render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+
+const start = vi.fn()
+const getCameras = vi.fn()
+
+vi.mock('html5-qrcode', () => ({
+  Html5Qrcode: class {
+    static getCameras = getCameras
+    start = start
+    stop = vi.fn().mockResolvedValue(undefined)
+    clear = vi.fn()
+    getState = () => 1
+  },
+  Html5QrcodeSupportedFormats: {
+    CODE_128: 0,
+    CODE_39: 1,
+    EAN_13: 2,
+    EAN_8: 3,
+    UPC_A: 4,
+    UPC_E: 5,
+    ITF: 6,
+    QR_CODE: 7,
+  },
+}))
+
+const { BarcodeScannerDialog } = await import('@/components/barcode-scanner-dialog')
+
+/** What html5-qrcode rejects with when the browser refuses the camera. */
+const DENIED = 'Error getting userMedia, error = NotAllowedError: Permission denied'
+
+function setSecure(secure: boolean) {
+  Object.defineProperty(window, 'isSecureContext', { value: secure, configurable: true })
+}
+
+function renderDialog() {
+  const onScan = vi.fn()
+  const onOpenChange = vi.fn()
+  render(<BarcodeScannerDialog open onOpenChange={onOpenChange} onScan={onScan} title="Scan" />)
+  return { onScan, onOpenChange }
+}
+
+/** A browser that answers the Permissions API with this camera state. */
+function setCameraPermission(state: PermissionState) {
+  const listeners = new Set<() => void>()
+  const status = {
+    state,
+    addEventListener: (_: string, fn: () => void) => listeners.add(fn),
+    removeEventListener: (_: string, fn: () => void) => listeners.delete(fn),
+  }
+  Object.defineProperty(navigator, 'permissions', {
+    value: { query: vi.fn().mockResolvedValue(status) },
+    configurable: true,
+  })
+  return {
+    change(next: PermissionState) {
+      status.state = next
+      for (const fn of [...listeners]) fn()
+    },
+  }
+}
+
+beforeEach(() => {
+  start.mockReset()
+  getCameras.mockReset()
+  setSecure(true)
+})
+
+afterEach(() => {
+  Object.defineProperty(navigator, 'permissions', { value: undefined, configurable: true })
+})
+
+describe('BarcodeScannerDialog', () => {
+  it('says the camera is blocked and offers typing at once, asking only once', async () => {
+    start.mockRejectedValue(DENIED)
+    renderDialog()
+
+    expect(await screen.findByText('Camera access is blocked')).toBeInTheDocument()
+    expect(screen.getByRole('textbox', { name: 'Enter barcode...' })).toBeInTheDocument()
+    expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument()
+    expect(start).toHaveBeenCalledTimes(1)
+    expect(getCameras).not.toHaveBeenCalled()
+  })
+
+  it('takes a typed code on the first press', async () => {
+    start.mockRejectedValue(DENIED)
+    const { onScan, onOpenChange } = renderDialog()
+    const user = userEvent.setup()
+
+    await user.type(await screen.findByRole('textbox'), ' 7031234567890 ')
+    await user.click(screen.getByRole('button', { name: 'Go' }))
+
+    expect(onScan).toHaveBeenCalledTimes(1)
+    expect(onScan).toHaveBeenCalledWith('7031234567890')
+    expect(onOpenChange).toHaveBeenCalledWith(false)
+  })
+
+  it('starts the camera again from Try again', async () => {
+    start.mockRejectedValueOnce(DENIED).mockResolvedValue(null)
+    renderDialog()
+    const user = userEvent.setup()
+
+    await user.click(await screen.findByRole('button', { name: 'Try again' }))
+
+    await vi.waitFor(() => expect(start).toHaveBeenCalledTimes(2))
+    expect(screen.queryByText('Camera access is blocked')).not.toBeInTheDocument()
+  })
+
+  it('starts the camera by itself once the browser allows it', async () => {
+    const permission = setCameraPermission('denied')
+    start.mockRejectedValueOnce(DENIED).mockResolvedValue(null)
+    renderDialog()
+    await screen.findByText('Camera access is blocked')
+
+    act(() => permission.change('granted'))
+
+    await vi.waitFor(() => expect(start).toHaveBeenCalledTimes(2))
+  })
+
+  it('restarts once, not in circles, when the browser allows what the system refuses', async () => {
+    // Allowed in the browser, refused by the operating system: every start
+    // fails the same way while the permission reads granted.
+    setCameraPermission('granted')
+    start.mockRejectedValue(DENIED)
+    renderDialog()
+
+    await vi.waitFor(() => expect(start).toHaveBeenCalledTimes(2))
+    expect(await screen.findByText('Camera access is blocked')).toBeInTheDocument()
+    await new Promise((r) => setTimeout(r, 300))
+    expect(start).toHaveBeenCalledTimes(2)
+  })
+
+  it('falls back to any camera when the rear one fails for another reason', async () => {
+    start.mockRejectedValueOnce('Error getting userMedia, error = OverconstrainedError')
+    start.mockResolvedValue(null)
+    getCameras.mockResolvedValue([{ id: 'usb-cam', label: 'USB camera' }])
+    renderDialog()
+
+    await vi.waitFor(() => expect(start).toHaveBeenCalledTimes(2))
+    expect(start.mock.calls[1][0]).toBe('usb-cam')
+  })
+
+  it('explains HTTPS without asking for the camera on an insecure page', async () => {
+    setSecure(false)
+    renderDialog()
+
+    expect(await screen.findByText('The camera needs HTTPS')).toBeInTheDocument()
+    expect(screen.queryByRole('button', { name: 'Try again' })).not.toBeInTheDocument()
+    expect(start).not.toHaveBeenCalled()
+  })
+})

+ 128 - 42
src/components/barcode-scanner-dialog.tsx

@@ -10,7 +10,7 @@ import {
 } from '@/components/ui/dialog'
 } from '@/components/ui/dialog'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Input } from '@/components/ui/input'
-import { Camera, Keyboard } from 'lucide-react'
+import { Camera, CameraOff, Keyboard, RotateCcw } from 'lucide-react'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 
 
 interface BarcodeScannerDialogProps {
 interface BarcodeScannerDialogProps {
@@ -20,6 +20,26 @@ interface BarcodeScannerDialogProps {
   title?: string
   title?: string
 }
 }
 
 
+/** Why the camera is not showing: each has its own sentence and way forward. */
+type CameraProblem = 'denied' | 'insecure' | 'notFound' | 'failed'
+
+/** html5-qrcode rejects with a sentence string; the browser with a DOMException. */
+function describeError(err: unknown): string {
+  if (typeof err === 'string') return err
+  if (err && typeof err === 'object') {
+    const { name, message } = err as { name?: unknown; message?: unknown }
+    return `${typeof name === 'string' ? name : ''} ${typeof message === 'string' ? message : ''}`
+  }
+  return String(err)
+}
+
+function problemOf(err: unknown): CameraProblem {
+  const text = describeError(err)
+  if (/NotAllowedError|Permission|denied/i.test(text)) return 'denied'
+  if (/NotFoundError|DevicesNotFound|Requested device not found/i.test(text)) return 'notFound'
+  return 'failed'
+}
+
 export function BarcodeScannerDialog({
 export function BarcodeScannerDialog({
   open,
   open,
   onOpenChange,
   onOpenChange,
@@ -29,14 +49,29 @@ export function BarcodeScannerDialog({
   const t = useTranslations('common.shared')
   const t = useTranslations('common.shared')
   const scannerRef = useRef<HTMLDivElement>(null)
   const scannerRef = useRef<HTMLDivElement>(null)
   const html5QrRef = useRef<any>(null)
   const html5QrRef = useRef<any>(null)
-  const [error, setError] = useState<string | null>(null)
+  const [problem, setProblem] = useState<CameraProblem | null>(null)
   const [showManual, setShowManual] = useState(false)
   const [showManual, setShowManual] = useState(false)
   const [manualValue, setManualValue] = useState('')
   const [manualValue, setManualValue] = useState('')
   const hasScannedRef = useRef(false)
   const hasScannedRef = useRef(false)
+  // The one restart the dialog makes by itself each time it opens; see below.
+  const autoRetriedRef = useRef(false)
 
 
   const scanBufferRef = useRef<string[]>([])
   const scanBufferRef = useRef<string[]>([])
   const REQUIRED_CONSISTENT_READS = 3
   const REQUIRED_CONSISTENT_READS = 3
 
 
+  const accept = useCallback(
+    (code: string) => {
+      if (hasScannedRef.current) return
+      hasScannedRef.current = true
+      scanBufferRef.current = []
+      onScan(code)
+      onOpenChange(false)
+    },
+    [onScan, onOpenChange]
+  )
+
+  // The camera has to read the same code a few frames in a row before it
+  // counts; a typed code is taken as it is.
   const handleScan = useCallback(
   const handleScan = useCallback(
     (decodedText: string) => {
     (decodedText: string) => {
       if (hasScannedRef.current) return
       if (hasScannedRef.current) return
@@ -50,25 +85,33 @@ export function BarcodeScannerDialog({
 
 
       buffer.push(decodedText)
       buffer.push(decodedText)
       if (buffer.length < REQUIRED_CONSISTENT_READS) return
       if (buffer.length < REQUIRED_CONSISTENT_READS) return
-
-      // Same barcode confirmed N times in a row — accept it
-      hasScannedRef.current = true
-      scanBufferRef.current = []
-      onScan(decodedText)
-      onOpenChange(false)
+      accept(decodedText)
     },
     },
-    [onScan, onOpenChange]
+    [accept]
   )
   )
 
 
+  const retryCamera = useCallback(() => {
+    setProblem(null)
+    setShowManual(false)
+  }, [])
+
   useEffect(() => {
   useEffect(() => {
     if (!open || showManual) {
     if (!open || showManual) {
       hasScannedRef.current = false
       hasScannedRef.current = false
+      if (!open) autoRetriedRef.current = false
+      return
+    }
+
+    // Browsers only hand out the camera on HTTPS or localhost; asking would
+    // only fail, so say why straight away.
+    if (!window.isSecureContext) {
+      setProblem('insecure')
+      setShowManual(true)
       return
       return
     }
     }
 
 
     let scanner: any = null
     let scanner: any = null
     let mounted = true
     let mounted = true
-    let running = false
     scanBufferRef.current = []
     scanBufferRef.current = []
 
 
     const startScanner = async () => {
     const startScanner = async () => {
@@ -116,37 +159,22 @@ export function BarcodeScannerDialog({
         try {
         try {
           // Try rear camera first (mobile)
           // Try rear camera first (mobile)
           await scanner.start({ facingMode: 'environment' }, scanConfig, onSuccess, onError)
           await scanner.start({ facingMode: 'environment' }, scanConfig, onSuccess, onError)
-        } catch {
+        } catch (first) {
+          // A refused camera stays refused: asking again for any camera would
+          // only show the browser's prompt, or its refusal, a second time.
+          if (problemOf(first) === 'denied') throw first
           // Fall back to any available camera (desktop USB/webcam)
           // Fall back to any available camera (desktop USB/webcam)
           const devices = await Html5Qrcode.getCameras()
           const devices = await Html5Qrcode.getCameras()
           if (!devices.length) throw new Error('NotFoundError')
           if (!devices.length) throw new Error('NotFoundError')
           await scanner.start(devices[0].id, scanConfig, onSuccess, onError)
           await scanner.start(devices[0].id, scanConfig, onSuccess, onError)
         }
         }
-        running = true
-      } catch (err: any) {
+      } catch (err: unknown) {
         if (!mounted) return
         if (!mounted) return
-        console.error('[BarcodeScan] Camera error:', err?.name, err?.message, err)
-
-        const isInsecure =
-          window.location.protocol === 'http:' &&
-          window.location.hostname !== 'localhost' &&
-          window.location.hostname !== '127.0.0.1'
-
-        if (isInsecure) {
-          setError(
-            `Camera requires HTTPS. You are on HTTP (${window.location.hostname}). Use localhost or enable HTTPS for camera access. You can enter the barcode manually below.`
-          )
-        } else if (err?.name === 'NotAllowedError' || err?.message?.includes('Permission')) {
-          setError(
-            'Camera access denied. Please allow camera permissions or enter the barcode manually.'
-          )
-        } else if (err?.name === 'NotFoundError' || err?.message === 'NotFoundError') {
-          setError('No camera found on this device. Please enter the barcode manually.')
-        } else {
-          setError(
-            `Could not start camera: ${err?.message || 'Unknown error'}. Please enter the barcode manually.`
-          )
-        }
+        const found = problemOf(err)
+        // A refused or missing camera is the person's setup, not a fault.
+        if (found === 'failed') console.error('[BarcodeScan] Camera error:', describeError(err))
+        else console.warn('[BarcodeScan] Camera unavailable:', describeError(err))
+        setProblem(found)
         setShowManual(true)
         setShowManual(true)
       }
       }
     }
     }
@@ -173,17 +201,47 @@ export function BarcodeScannerDialog({
     }
     }
   }, [open, showManual, handleScan])
   }, [open, showManual, handleScan])
 
 
+  // Allowing the camera in the browser while the dialog is open starts it,
+  // where the browser says so (the Permissions API is not everywhere). Allowed
+  // already by the time the answer comes counts as well. Once per opening: the
+  // browser can allow a camera the system still refuses (macOS privacy
+  // settings), and restarting on every refusal would go round in circles.
+  // Try again is always there for a second go.
+  useEffect(() => {
+    if (!open || problem !== 'denied' || !navigator.permissions?.query) return
+    let status: PermissionStatus | null = null
+    let cancelled = false
+    const restartIfAllowed = () => {
+      if (cancelled || status?.state !== 'granted' || autoRetriedRef.current) return
+      autoRetriedRef.current = true
+      retryCamera()
+    }
+    navigator.permissions
+      .query({ name: 'camera' as PermissionName })
+      .then((s) => {
+        if (cancelled) return
+        status = s
+        s.addEventListener('change', restartIfAllowed)
+        restartIfAllowed()
+      })
+      .catch(() => {
+        /* this browser cannot be asked about the camera */
+      })
+    return () => {
+      cancelled = true
+      status?.removeEventListener('change', restartIfAllowed)
+    }
+  }, [open, problem, retryCamera])
+
   const handleManualSubmit = (e: React.FormEvent) => {
   const handleManualSubmit = (e: React.FormEvent) => {
     e.preventDefault()
     e.preventDefault()
     const trimmed = manualValue.trim()
     const trimmed = manualValue.trim()
-    if (trimmed) {
-      handleScan(trimmed)
-    }
+    if (trimmed) accept(trimmed)
   }
   }
 
 
   const handleOpenChange = (v: boolean) => {
   const handleOpenChange = (v: boolean) => {
     if (!v) {
     if (!v) {
-      setError(null)
+      setProblem(null)
       setShowManual(false)
       setShowManual(false)
       setManualValue('')
       setManualValue('')
     }
     }
@@ -217,19 +275,47 @@ export function BarcodeScannerDialog({
           </div>
           </div>
         ) : (
         ) : (
           <div className="space-y-3">
           <div className="space-y-3">
-            {error && <p className="text-sm text-muted-foreground">{error}</p>}
+            {problem && (
+              <div
+                role="alert"
+                className="flex gap-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3"
+              >
+                <CameraOff className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
+                <div className="min-w-0 space-y-1">
+                  <p className="text-sm font-medium">{t(`camera.${problem}.title`)}</p>
+                  <p className="text-xs text-muted-foreground">
+                    {problem === 'insecure'
+                      ? t('camera.insecure.body', { host: window.location.hostname })
+                      : t(`camera.${problem}.body`)}
+                  </p>
+                  {problem !== 'insecure' && (
+                    <Button
+                      type="button"
+                      variant="outline"
+                      size="sm"
+                      className="mt-1 h-7 bg-background px-2.5 text-xs"
+                      onClick={retryCamera}
+                    >
+                      <RotateCcw className="h-3 w-3" />
+                      {t('camera.tryAgain')}
+                    </Button>
+                  )}
+                </div>
+              </div>
+            )}
             <form onSubmit={handleManualSubmit} className="flex gap-2">
             <form onSubmit={handleManualSubmit} className="flex gap-2">
               <Input
               <Input
                 placeholder={t('enterBarcode')}
                 placeholder={t('enterBarcode')}
+                aria-label={t('enterBarcode')}
                 value={manualValue}
                 value={manualValue}
                 onChange={(e) => setManualValue(e.target.value)}
                 onChange={(e) => setManualValue(e.target.value)}
                 autoFocus
                 autoFocus
               />
               />
               <Button type="submit" disabled={!manualValue.trim()}>
               <Button type="submit" disabled={!manualValue.trim()}>
-                Go
+                {t('submitBarcode')}
               </Button>
               </Button>
             </form>
             </form>
-            {!error && (
+            {!problem && (
               <Button
               <Button
                 type="button"
                 type="button"
                 variant="outline"
                 variant="outline"