Repository: web2py/web2py

Issue:
UF-8 error
**Describe the bug**
Translation files in Languages, f.i. language/de.py in the welcome app may contain characters that cause this error:
<type 'exceptions.UnicodeDecodeError'> 'utf8' codec can't decode byte 0xa0 in position 8: invalid start byte
Tested for German, may exist in other language files as well. 

**To Reproduce**
Steps to reproduce the behavior:
1. Set Browser Language to 'German' 
2. Visit Welcome index.html (http://127.0.0.1:8000/welcome)

**Expected behavior**
Page should load and not show a type exception error
**Screenshots**
![character](https://user-images.githubusercontent.com/4863529/209942450-8b7911b5-8854-40fa-97c9-7278e436ca13.jpg)

**Additional context**
After cleaning the file, the error disappears. The faulty character might be introduced into the language file, once the system adds new text marked with T(...) to the file. The character seems to stand for a whitespace 



Discussion:

Resolving patch:
diff --git a/applications/welcome/languages/de.py b/applications/welcome/languages/de.py
--- a/applications/welcome/languages/de.py
+++ b/applications/welcome/languages/de.py
@@ -3,42 +3,46 @@
 '!langcode!': 'de',
 '!langname!': 'Deutsch (DE)',
 '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"Update" ist ein optionaler Ausdruck wie "feld1=\'newvalue\'". JOIN Ergebnisse können nicht aktualisiert oder gelöscht werden',
-'%Y-%m-%d': '%Y-%m-%d',
-'%Y-%m-%d %H:%M:%S': '%Y-%m-%d\xa0%H:%M:%S',
 '%s %%{row} deleted': '%s %%{row} gelöscht',
 '%s %%{row} updated': '%s %%{row} aktualisiert',
 '%s selected': '%s ausgewählt',
-'(**%.0d MB**)': '(**%.0d\xa0MB**)',
+'%Y-%m-%d': '%Y-%m-%d',
+'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S',
+'(**%.0d MB**)': '(**%.0d MB**)',
 '**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{Item(Items)}, **%(bytes)s** %%{Byte(Bytes)}',
 '**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** Items, **%(bytes)s** %%{Byte(Bytes)}',
 '**not available** (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '**nicht verfügbar** (benötigt die Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] Bibliothek)',
 '?': '?',
-'@markmin\x01(**%.0d MB**)': '(**%.0d\xa0MB**)',
+'@markmin\x01(**%.0d MB**)': '(**%.0d MB**)',
 '@markmin\x01**%(items)s** %%{item(items)}, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** %%{Item(items)}, **%(bytes)s** %%{Byte(bytes)}',
 '@markmin\x01**%(items)s** items, **%(bytes)s** %%{byte(bytes)}': '**%(items)s** items, **%(bytes)s** %%{Byte(bytes)}',
 '@markmin\x01**not available** (requires the Python [[Pympler https://pypi.python.org/pypi/Pympler popup]] library)': '**nicht verfügbar** (requires the Python [[Pympler https://pypi.python.org/pypi/Pympler popup]] library)',
+'@markmin\x01``**not available**``:red (requires the Python [[Pympler https://pypi.python.org/pypi/Pympler popup]] library)': '``**nicht verfügbar**``:red (requires the Python [[Pympler https://pypi.python.org/pypi/Pympler popup]] library)',
 '@markmin\x01An error occured, please [[reload %s]] the page': 'Hi ha hagut un error, si us plau [[recarregui %s]] la pàgina',
 '@markmin\x01Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache geleert enthält Artikel bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
 '@markmin\x01DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK enthält Artikel bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
 '@markmin\x01Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Trefferquote:**%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})',
 '@markmin\x01Number of entries: **%s**': "Nombre d'entrades: **%s**",
-'@markmin\x01RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM enthält Elemente bis zu\xa0**%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
-'@markmin\x01``**not available**``:red (requires the Python [[Pympler https://pypi.python.org/pypi/Pympler popup]] library)': '``**nicht verfügbar**``:red (requires the Python [[Pympler https://pypi.python.org/pypi/Pympler popup]] library)',
+'@markmin\x01RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM enthält Elemente bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.',
+'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**nicht verfügbar**``:rot (benötigt die Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] Bibliothek)',
 'A new password was emailed to you': 'Ein neues Passwort wurde per E-Mail an Sie gesendet',
-'API Example': 'API Beispiel',
 'About': 'Über',
 'Access Control': 'Zugangskontrolle',
+'admin': 'admin',
 'Ajax Recipes': 'Ajax Rezepte',
 'An error occured, please [[reload %s]] the page': 'Ein Fehler ist aufgetreten, bitte [[laden %s]] Sie die Seite neu',
+'API Example': 'API Beispiel',
+'appadmin is disabled because insecure channel': 'Appadmin ist deaktiviert, wegen der Benutzung eines unsicheren Kanals',
 'Apply changes': 'Aplicar canvis',
 'Are you sure you want to delete this object?': 'Sind Sie sich sicher, dass Sie dieses Objekt löschen wollen?',
 'Authentication code': 'Authentifizierungs code',
 'Available Databases and Tables': 'Verfügbare Datenbanken und Tabellen',
 "Buy web2py's book": "web2py's Buch kaufen",
+'cache': 'zwischenspeicher',
 'Cache': 'Zwischenspeicher',
 'Cache Cleared': 'Cache geleert',
-'Cache Keys': 'Cache Schlüssel',
 'Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'Cache enthält items die bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} alt sind.',
+'Cache Keys': 'Cache Schlüssel',
 'Cannot be empty': 'Darf nicht leer sein',
 'Change Password': 'Canviï la Contrasenya',
 'Change password': 'Canviï la contrasenya',
@@ -56,21 +60,25 @@
 'Current request': 'Derzeitiger Request',
 'Current response': 'Derzeitige Response',
 'Current session': 'Derzeitige Session',
-'DB Model': 'Muster-DB',
-'DISK': 'DISK',
-'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK enthält items die bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} alt sind.',
+'data uploaded': 'Datei hochgeladen',
 'Database': 'Datenbank',
 'Database %s select': 'Datenbank %s ausgewählt',
 'Database Administration (appadmin)': 'Datenbankadministration (appadmin)',
+'db': 'db',
+'DB Model': 'Muster-DB',
 'Delete:': 'Lösche:',
 'Demo': 'Demo',
 'Deployment Recipes': 'Entwicklungsrezepte',
 'Description': 'Descripció',
+'design': 'Design',
 'Design': 'Design',
+'DISK': 'DISK',
 'Disk Cache Keys': 'Festplatten-Cache-Schlüssel',
 'Disk Cleared': 'Disk gelöscht',
+'DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'DISK enthält items die bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} alt sind.',
 'Documentation': 'Dokumentation',
 "Don't know what to do?": 'Wissen Sie nicht weiter?',
+'done!': 'Fertig!',
 'Download': 'Download',
 'E-mail': 'Correu electrònic',
 'Edit current record': 'Diesen Eintrag editieren',
@@ -79,6 +87,7 @@
 'Email verification': 'Email-Verifizierung',
 'Email verified': 'Email-Verifiziert',
 'Errors': 'Fehlermeldungen',
+'export as csv file': 'als csv Datei exportieren',
 'FAQ': 'FAQ',
 'First name': 'Nom',
 'Forms and Validators': 'Forms und Validators',
@@ -96,16 +105,18 @@
 'Hit Ratio: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} and **%(misses)s** %%{miss(misses)})': 'Trefferquote: **%(ratio)s%%** (**%(hits)s** %%{hit(hits)} und **%(misses)s** %%{miss(misses)})',
 'Home': 'Startseite',
 'How did you get here?': 'Wie sind Sie hier her gelangt?',
+'import': 'Importieren',
 'Import/Export': 'Importieren/Exportieren',
 'Incorrect code. {0} more attempt(s) remaining.': 'Falscher Code. {0} weitere Versuche verbleiben.',
 'Insufficient privileges': 'Privilegis insuficients',
 'Internal State': 'Innerer Zustand',
 'Introduction': 'Einführung',
-'Invalid Query': 'Ungültige Query',
 'Invalid email': 'Correo electrónico invàlid',
 'Invalid key': 'Ungültiger Schlüssel',
 'Invalid login': 'Inici de sessió invàlida',
 'Invalid password': 'Ungültiges Passwort',
+'Invalid Query': 'Ungültige Query',
+'invalid request': 'Ungültiger Request',
 'Invalid reset password': 'Reinici de contrasenya invàlid',
 'Invalid user': 'Ungültiger Benutzer',
 'Invalid username': 'Ungültiger Benutzername',
@@ -130,14 +141,17 @@
 'Menu Model': 'Menü-Muster',
 'My Sites': 'Meine Seiten',
 'Name': 'Nombre',
-'New Record': 'Neuer Eintrag',
 'New password': 'Contrasenya nova',
+'New Record': 'Neuer Eintrag',
+'new record inserted': 'neuer Eintrag hinzugefügt',
+'next %s rows': 'nächste %s Reihen',
 'No databases in this application': 'Keine Datenbank in dieser Anwendung',
 'Number of entries: **%s**': 'Nummer der Einträge: **%s**',
 'Object or table name': 'Nom del objecte o taula',
 'Old password': 'Contrasenya anterior',
 'Online book': 'Online Buch',
 'Online examples': 'Online Beispiele',
+'or import from csv file': 'oder von csv Datei importieren',
 'Origin': 'Origen',
 'Other Recipes': 'Andere Rezepte',
 'Overview': 'Überblick',
@@ -148,18 +162,21 @@
 'Password retrieve': 'Passwort abrufen',
 'Permission': 'Erlaubnis',
 'Permissions': 'Erlaubnisse',
+'please input your password again': 'si us plau, entri un altre cop la seva contrasenya',
 'Plugins': 'Plugins',
 'Powered by': 'Unterstützt von',
 'Preface': 'Allgemeines',
+'previous %s rows': 'vorherige %s Reihen',
 'Profile': 'Perfil',
 'Profile updated': 'Perfil actualitzat',
+'pygraphviz library not found': 'pygraphviz Bibliothek wurde nicht gefunden',
 'Python': 'Python',
 'Query:': 'Abfrage:',
 'Quick Examples': 'Kurze Beispiele',
 'RAM': 'RAM',
 'RAM Cache Keys': 'RAM Cache-Schlüssel',
-'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM enthält items die bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} alt sind.',
 'Ram Cleared': 'Ram gelöscht',
+'RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.': 'RAM enthält items die bis zu **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} alt sind.',
 'Recipes': 'Rezepte',
 'Record': 'Eintrag',
 'Record %(id)s created': 'Registre %(id)s creat',
@@ -168,9 +185,10 @@
 'Record %(id)s updated': 'Eintrag %(id)s aktualisiert',
 'Record Created': 'Registre Creat',
 'Record Deleted': 'Eintrag gelöscht',
+'record does not exist': 'Eintrag existiert nicht',
 'Record ID': 'ID de Registre',
-'Record Updated': 'Eintrag Aktualisiert',
 'Record id': 'id des Eintrags',
+'Record Updated': 'Eintrag Aktualisiert',
 'Register': "Registri's",
 'Registration identifier': 'Identificador de Registre',
 'Registration is pending approval': 'Die Registrierung steht noch aus',
@@ -189,15 +207,17 @@
 'Sign Up': 'Registrieren',
 'Sign up': 'Anmelden',
 'Size of cache:': 'Cachegröße:',
+'state': 'Status',
 'Statistics': 'Statistik',
 'Stylesheet': 'Stylesheet',
+'submit': 'Submit',
 'Submit': 'Enviar',
 'Support': 'Support',
 'Table': 'Tabelle',
 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'Die "query" ist eine Bedingung wie "db.tabelle1.feld1==\'wert\'". So etwas wie "db.tabelle1.feld1==db.tabelle2.feld2" resultiert in einem SQL JOIN.',
 'The Core': 'Der Core',
-'The Views': 'Die Views',
 'The output of the file is a dictionary that was rendered by the view %s': 'Die Ausgabe der Datei ist ein "dictionary", welches vom "view" %s gerendert wurde',
+'The Views': 'Die Views',
 'This App': 'Diese App',
 'This code was emailed to you and is required for login.': 'Dieser Code wurde per E-Mail an Sie gesendet und ist für die Anmeldung erforderlich.',
 'This email already has an account': 'Aquest correu electrònic ja té un compte',
@@ -206,10 +226,12 @@
 'Traceback': 'Zurückverfolgen',
 'Twitter': 'Twitter',
 'Two-step Login Authentication Code': 'Zweistufiger Login-Authentifizierungscode',
+'unable to parse csv file': 'csv Datei konnte nicht geparst werden',
 'Unable to send email': 'Email kann nicht gesendet werden',
 'Update:': 'Update:',
 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...)  for NOT to build more complex queries.': 'Benutze (...)&(...) für AND, (...)|(...) für OR, und ~(...)  für NOT um komplexere Queries zu erstellen.',
 'User': 'Benutzer',
+'User %(id)s is impersonating %(other_id)s': 'El usuari %(id)s està suplantant %(other_id)s',
 'User %(id)s Logged-in': 'El usuari %(id)s inicià la sessió',
 'User %(id)s Logged-out': 'El usuari %(id)s finalitzà la sessió',
 'User %(id)s Password changed': 'Contrasenya del usuari %(id)s canviada',
@@ -219,7 +241,6 @@
 'User %(id)s Registered': 'Usuari %(id)s Registrat',
 'User %(id)s Username retrieved': 'Se ha recuperat el nom de usuari del usuari %(id)s',
 'User %(id)s Verification email sent': 'Benutzer %(id)s Bestätigungs-Email gesendet',
-'User %(id)s is impersonating %(other_id)s': 'El usuari %(id)s està suplantant %(other_id)s',
 'User %(id)s verified registration key': 'Benutzer %(id)s verifizierter Registrierungsschlüssel',
 'User ID': 'ID de Usuari',
 'Username': 'Nom de usuari',
@@ -242,25 +263,4 @@
 'Your temporary login code is {0}': 'Ihr temporärer Login-Code lautet {0}',
 'Your username is: %(username)s': 'El seu nom de usuari és: %(username)s',
 'Your username was emailed to you': 'Ihr Benutzername wurde per E-Mail an Sie gesendet',
-'``**not available**``:red (requires the Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] library)': '``**nicht verfügbar**``:rot (benötigt die Python [[guppy http://pypi.python.org/pypi/guppy/ popup]] Bibliothek)',
-'admin': 'admin',
-'appadmin is disabled because insecure channel': 'Appadmin ist deaktiviert, wegen der Benutzung eines unsicheren Kanals',
-'cache': 'zwischenspeicher',
-'data uploaded': 'Datei hochgeladen',
-'db': 'db',
-'design': 'Design',
-'done!': 'Fertig!',
-'export as csv file': 'als csv Datei exportieren',
-'import': 'Importieren',
-'invalid request': 'Ungültiger Request',
-'new record inserted': 'neuer Eintrag hinzugefügt',
-'next %s rows': 'nächste %s Reihen',
-'or import from csv file': 'oder von csv Datei importieren',
-'please input your password again': 'si us plau, entri un altre cop la seva contrasenya',
-'previous %s rows': 'vorherige %s Reihen',
-'pygraphviz library not found': 'pygraphviz Bibliothek wurde nicht gefunden',
-'record does not exist': 'Eintrag existiert nicht',
-'state': 'Status',
-'submit': 'Submit',
-'unable to parse csv file': 'csv Datei konnte nicht geparst werden',
 }


Test patch: