From 67bdfa3ad6c6eb36da8aca2af5b6b580b0816a9d Mon Sep 17 00:00:00 2001
From: Yannick Schaus <github@schaus.net>
Date: Sat, 12 Dec 2020 22:42:56 +0100
Subject: [PATCH] Auth pages i18n (#1913)

This implements localized messages for the authorize, change
password and create API token pages using a resource bundle.

Messages in English & French are included.

Signed-off-by: Yannick Schaus <github@schaus.net>
---
 .../org.openhab.core.io.http.auth/.classpath  |  1 +
 .../pages/authorize.html                      | 14 ++++----
 .../internal/AbstractAuthPageServlet.java     | 35 +++++++++++++++++--
 .../auth/internal/AuthorizePageServlet.java   | 19 +++++-----
 .../internal/ChangePasswordPageServlet.java   | 17 ++++-----
 .../internal/CreateAPITokenPageServlet.java   | 30 ++++++++--------
 .../src/main/resources/messages.properties    | 26 ++++++++++++++
 .../src/main/resources/messages_fr.properties | 26 ++++++++++++++
 8 files changed, 127 insertions(+), 41 deletions(-)
 create mode 100644 bundles/org.openhab.core.io.http.auth/src/main/resources/messages.properties
 create mode 100644 bundles/org.openhab.core.io.http.auth/src/main/resources/messages_fr.properties

diff --git a/bundles/org.openhab.core.io.http.auth/.classpath b/bundles/org.openhab.core.io.http.auth/.classpath
index 01095f5fb4..534c918e68 100644
--- a/bundles/org.openhab.core.io.http.auth/.classpath
+++ b/bundles/org.openhab.core.io.http.auth/.classpath
@@ -6,6 +6,7 @@
 			<attribute name="maven.pomderived" value="true"/>
 		</attributes>
 	</classpathentry>
+	<classpathentry kind="src" path="src/main/resources"/>
 	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
 		<attributes>
 			<attribute name="maven.pomderived" value="true"/>
diff --git a/bundles/org.openhab.core.io.http.auth/pages/authorize.html b/bundles/org.openhab.core.io.http.auth/pages/authorize.html
index 3c994ac1e3..9b24fe84ee 100644
--- a/bundles/org.openhab.core.io.http.auth/pages/authorize.html
+++ b/bundles/org.openhab.core.io.http.auth/pages/authorize.html
@@ -102,29 +102,29 @@ input.submit:hover {
 	<form class="{formClass}" method="POST" action="{formAction}">
 		{form_fields}
 		<div>
-			<input class="field" autocomplete="off" type="text" placeholder="User name" name="username" required autofocus />
+			<input class="field" autocomplete="off" type="text" placeholder="{usernamePlaceholder}" name="username" required autofocus />
 		</div>
 		<div>
-			<input class="field" type="password" placeholder="Password" name="password" required />
+			<input class="field" type="password" placeholder="{passwordPlaceholder}" name="password" required />
 		</div>
 		<div>
-			<input class="field" type="{newPasswordFieldType}" placeholder="New Password" name="new_password" />
+			<input class="field" type="{newPasswordFieldType}" placeholder="{newPasswordPlaceholder}" name="new_password" />
 		</div>
 		<div>
-			<input class="field" type="{repeatPasswordFieldType}" placeholder="Confirm New Password" name="password_repeat" />
+			<input class="field" type="{repeatPasswordFieldType}" placeholder="{repeatPasswordPlaceholder}" name="password_repeat" />
 		</div>
 		<div>
-			<input class="field" type="{tokenNameFieldType}" placeholder="Token Name" name="token_name" />
+			<input class="field" type="{tokenNameFieldType}" placeholder="{tokenNamePlaceholder}" name="token_name" />
 		</div>
 		<div>
-			<input class="field" type="{tokenScopeFieldType}" placeholder="Token Scope (optional)" name="token_scope" />
+			<input class="field" type="{tokenScopeFieldType}" placeholder="{tokenScopePlaceholder}" name="token_scope" />
 		</div>
 		<div>
 			<input class="submit" type="Submit" value="{buttonLabel}" />
 		</div>
 	</form>
 	<div class="result{resultClass}">
-		<a class="submit" type="button" href="/">Return Home</a>
+		<a class="submit" type="button" href="/">{returnButtonLabel}</a>
 	</div>
 </body>
 </html>
diff --git a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java
index 0511074be5..217ec69629 100644
--- a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java
+++ b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AbstractAuthPageServlet.java
@@ -21,6 +21,8 @@ import java.time.Duration;
 import java.time.Instant;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.ResourceBundle.Control;
 import java.util.UUID;
 
 import javax.servlet.http.HttpServlet;
@@ -34,6 +36,7 @@ import org.openhab.core.auth.AuthenticationProvider;
 import org.openhab.core.auth.User;
 import org.openhab.core.auth.UserRegistry;
 import org.openhab.core.auth.UsernamePasswordCredentials;
+import org.openhab.core.i18n.LocaleProvider;
 import org.osgi.framework.BundleContext;
 import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.http.HttpService;
@@ -51,11 +54,14 @@ public abstract class AbstractAuthPageServlet extends HttpServlet {
 
     protected static final long serialVersionUID = 5340598701104679840L;
 
+    private static final String MESSAGES_BUNDLE_NAME = "messages";
+
     private final Logger logger = LoggerFactory.getLogger(AbstractAuthPageServlet.class);
 
     protected HttpService httpService;
     protected UserRegistry userRegistry;
     protected AuthenticationProvider authProvider;
+    protected LocaleProvider localeProvider;
     protected @Nullable Instant lastAuthenticationFailure;
     protected int authenticationFailureCount = 0;
 
@@ -64,10 +70,12 @@ public abstract class AbstractAuthPageServlet extends HttpServlet {
     protected String pageTemplate;
 
     public AbstractAuthPageServlet(BundleContext bundleContext, @Reference HttpService httpService,
-            @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider) {
+            @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider,
+            @Reference LocaleProvider localeProvider) {
         this.httpService = httpService;
         this.userRegistry = userRegistry;
         this.authProvider = authProvider;
+        this.localeProvider = localeProvider;
 
         pageTemplate = "";
         URL resource = bundleContext.getBundle().getResource("pages/authorize.html");
@@ -80,6 +88,23 @@ public abstract class AbstractAuthPageServlet extends HttpServlet {
         }
     }
 
+    protected String getPageTemplate() {
+        String template = pageTemplate;
+        for (String[] replace : new String[][] { //
+                { "{usernamePlaceholder}", "auth.placeholder.username" },
+                { "{passwordPlaceholder}", "auth.placeholder.password" },
+                { "{newPasswordPlaceholder}", "auth.placeholder.newpassword" },
+                { "{repeatPasswordPlaceholder}", "auth.placeholder.repeatpassword" },
+                { "{tokenNamePlaceholder}", "auth.placeholder.tokenname" },
+                { "{tokenScopePlaceholder}", "auth.placeholder.tokenscope" },
+                { "{returnButtonLabel}", "auth.button.return" } //
+        }) {
+            template = template.replace(replace[0], getLocalizedMessage(replace[1]));
+        }
+
+        return template;
+    }
+
     protected abstract String getPageBody(Map<String, String[]> params, String message, boolean hideForm);
 
     protected abstract String getFormFields(Map<String, String[]> params);
@@ -123,7 +148,13 @@ public abstract class AbstractAuthPageServlet extends HttpServlet {
         authenticationFailureCount += 1;
         resp.setContentType("text/html;charset=UTF-8");
         logger.warn("Authentication failed: {}", message);
-        resp.getWriter().append(getPageBody(params, "Please try again.", false)); // TODO: i18n
+        resp.getWriter().append(getPageBody(params, getLocalizedMessage("auth.login.fail"), false));
         resp.getWriter().close();
     }
+
+    protected String getLocalizedMessage(String messageKey) {
+        ResourceBundle rb = ResourceBundle.getBundle(MESSAGES_BUNDLE_NAME, localeProvider.getLocale(),
+                Control.getNoFallbackControl(Control.FORMAT_PROPERTIES));
+        return rb.getString(messageKey);
+    }
 }
diff --git a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthorizePageServlet.java b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthorizePageServlet.java
index 64ac54145a..b073cf23d1 100644
--- a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthorizePageServlet.java
+++ b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/AuthorizePageServlet.java
@@ -32,6 +32,7 @@ import org.openhab.core.auth.PendingToken;
 import org.openhab.core.auth.Role;
 import org.openhab.core.auth.User;
 import org.openhab.core.auth.UserRegistry;
+import org.openhab.core.i18n.LocaleProvider;
 import org.osgi.framework.BundleContext;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -63,8 +64,9 @@ public class AuthorizePageServlet extends AbstractAuthPageServlet {
 
     @Activate
     public AuthorizePageServlet(BundleContext bundleContext, @Reference HttpService httpService,
-            @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider) {
-        super(bundleContext, httpService, userRegistry, authProvider);
+            @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider,
+            @Reference LocaleProvider localeProvider) {
+        super(bundleContext, httpService, userRegistry, authProvider, localeProvider);
         try {
             httpService.registerServlet("/auth", this, null, null);
         } catch (NamespaceException | ServletException e) {
@@ -86,11 +88,10 @@ public class AuthorizePageServlet extends AbstractAuthPageServlet {
                 throw new IllegalArgumentException("invalid_request");
             }
 
-            // TODO: i18n
             if (isSignupMode()) {
-                message = "Create a first administrator account to continue.";
+                message = getLocalizedMessage("auth.createaccount.prompt");
             } else {
-                message = String.format("Sign in to grant <b>%s</b> access to <b>%s</b>:", scope, clientId);
+                message = String.format(getLocalizedMessage("auth.login.prompt"), scope, clientId);
             }
             resp.setContentType("text/html;charset=UTF-8");
             resp.getWriter().append(getPageBody(params, message, false));
@@ -153,8 +154,8 @@ public class AuthorizePageServlet extends AbstractAuthPageServlet {
                 // first verify the password confirmation and bail out if necessary
                 if (!params.containsKey("password_repeat") || !password.equals(params.get("password_repeat")[0])) {
                     resp.setContentType("text/html;charset=UTF-8");
-                    // TODO: i18n
-                    resp.getWriter().append(getPageBody(params, "Passwords don't match, please try again.", false));
+                    resp.getWriter()
+                            .append(getPageBody(params, getLocalizedMessage("auth.password.confirm.fail"), false));
                     resp.getWriter().close();
                     return;
                 }
@@ -202,9 +203,9 @@ public class AuthorizePageServlet extends AbstractAuthPageServlet {
 
     @Override
     protected String getPageBody(Map<String, String[]> params, String message, boolean hideForm) {
-        String responseBody = pageTemplate.replace("{form_fields}", getFormFields(params));
+        String responseBody = getPageTemplate().replace("{form_fields}", getFormFields(params));
         String repeatPasswordFieldType = isSignupMode() ? "password" : "hidden";
-        String buttonLabel = isSignupMode() ? "Create Account" : "Sign In"; // TODO: i18n
+        String buttonLabel = getLocalizedMessage(isSignupMode() ? "auth.button.createaccount" : "auth.button.signin");
         responseBody = responseBody.replace("{message}", message);
         responseBody = responseBody.replace("{formAction}", "/auth");
         responseBody = responseBody.replace("{formClass}", "show");
diff --git a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/ChangePasswordPageServlet.java b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/ChangePasswordPageServlet.java
index fdefbb8080..fa0dcfdd76 100644
--- a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/ChangePasswordPageServlet.java
+++ b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/ChangePasswordPageServlet.java
@@ -25,6 +25,7 @@ import org.openhab.core.auth.AuthenticationProvider;
 import org.openhab.core.auth.ManagedUser;
 import org.openhab.core.auth.User;
 import org.openhab.core.auth.UserRegistry;
+import org.openhab.core.i18n.LocaleProvider;
 import org.osgi.framework.BundleContext;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -51,8 +52,9 @@ public class ChangePasswordPageServlet extends AbstractAuthPageServlet {
 
     @Activate
     public ChangePasswordPageServlet(BundleContext bundleContext, @Reference HttpService httpService,
-            @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider) {
-        super(bundleContext, httpService, userRegistry, authProvider);
+            @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider,
+            @Reference LocaleProvider localeProvider) {
+        super(bundleContext, httpService, userRegistry, authProvider, localeProvider);
         try {
             httpService.registerServlet("/changePassword", this, null, null);
         } catch (NamespaceException | ServletException e) {
@@ -102,8 +104,7 @@ public class ChangePasswordPageServlet extends AbstractAuthPageServlet {
 
             if (!params.containsKey("password_repeat") || !newPassword.equals(params.get("password_repeat")[0])) {
                 resp.setContentType("text/html;charset=UTF-8");
-                // TODO: i18n
-                resp.getWriter().append(getPageBody(params, "Passwords don't match, please try again.", false));
+                resp.getWriter().append(getPageBody(params, getLocalizedMessage("auth.password.confirm.fail"), false));
                 resp.getWriter().close();
                 return;
             }
@@ -117,7 +118,7 @@ public class ChangePasswordPageServlet extends AbstractAuthPageServlet {
             }
 
             resp.setContentType("text/html;charset=UTF-8");
-            resp.getWriter().append(getResultPageBody(params, "Password changed.")); // TODO: i18n
+            resp.getWriter().append(getResultPageBody(params, getLocalizedMessage("auth.changepassword.success")));
             resp.getWriter().close();
         } catch (AuthenticationException e) {
             processFailedLogin(resp, params, e.getMessage());
@@ -126,8 +127,8 @@ public class ChangePasswordPageServlet extends AbstractAuthPageServlet {
 
     @Override
     protected String getPageBody(Map<String, String[]> params, String message, boolean hideForm) {
-        String responseBody = pageTemplate.replace("{form_fields}", getFormFields(params));
-        String buttonLabel = "Change Password"; // TODO: i18n
+        String responseBody = getPageTemplate().replace("{form_fields}", getFormFields(params));
+        String buttonLabel = getLocalizedMessage("auth.button.changepassword");
         responseBody = responseBody.replace("{message}", message);
         responseBody = responseBody.replace("{formAction}", "/changePassword");
         responseBody = responseBody.replace("{formClass}", hideForm ? "hide" : "show");
@@ -141,7 +142,7 @@ public class ChangePasswordPageServlet extends AbstractAuthPageServlet {
     }
 
     protected String getResultPageBody(Map<String, String[]> params, String message) {
-        String responseBody = pageTemplate.replace("{form_fields}", "");
+        String responseBody = getPageTemplate().replace("{form_fields}", "");
         responseBody = responseBody.replace("{message}", message);
         responseBody = responseBody.replace("{formAction}", "/changePassword");
         responseBody = responseBody.replace("{formClass}", "hide");
diff --git a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/CreateAPITokenPageServlet.java b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/CreateAPITokenPageServlet.java
index 2a470ef844..cbb4266b75 100644
--- a/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/CreateAPITokenPageServlet.java
+++ b/bundles/org.openhab.core.io.http.auth/src/main/java/org/openhab/core/io/http/auth/internal/CreateAPITokenPageServlet.java
@@ -25,6 +25,7 @@ import org.openhab.core.auth.AuthenticationProvider;
 import org.openhab.core.auth.ManagedUser;
 import org.openhab.core.auth.User;
 import org.openhab.core.auth.UserRegistry;
+import org.openhab.core.i18n.LocaleProvider;
 import org.osgi.framework.BundleContext;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -51,8 +52,9 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
 
     @Activate
     public CreateAPITokenPageServlet(BundleContext bundleContext, @Reference HttpService httpService,
-            @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider) {
-        super(bundleContext, httpService, userRegistry, authProvider);
+            @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider,
+            @Reference LocaleProvider localeProvider) {
+        super(bundleContext, httpService, userRegistry, authProvider, localeProvider);
         try {
             httpService.registerServlet("/createApiToken", this, null, null);
         } catch (NamespaceException | ServletException e) {
@@ -65,9 +67,8 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
         Map<String, String[]> params = req.getParameterMap();
 
         try {
-            String message = "Create a new API token to authorize external services.";
+            String message = getLocalizedMessage("auth.createapitoken.prompt");
 
-            // TODO: i18n
             resp.setContentType("text/html;charset=UTF-8");
             resp.getWriter().append(getPageBody(params, message, false));
             resp.getWriter().close();
@@ -112,18 +113,16 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
                 if (((ManagedUser) user).getApiTokens().stream()
                         .anyMatch(apiToken -> apiToken.getName().equals(tokenName))) {
                     resp.setContentType("text/html;charset=UTF-8");
-                    // TODO: i18n
                     resp.getWriter().append(
-                            getPageBody(params, "A token with the same name already exists, please try again.", false));
+                            getPageBody(params, getLocalizedMessage("auth.createapitoken.name.unique.fail"), false));
                     resp.getWriter().close();
                     return;
                 }
 
                 if (!tokenName.matches("[a-zA-Z0-9]*")) {
                     resp.setContentType("text/html;charset=UTF-8");
-                    // TODO: i18n
                     resp.getWriter().append(
-                            getPageBody(params, "Invalid token name, please use alphanumeric characters only.", false));
+                            getPageBody(params, getLocalizedMessage("auth.createapitoken.name.format.fail"), false));
                     resp.getWriter().close();
                     return;
                 }
@@ -132,11 +131,12 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
                 throw new AuthenticationException("User is not managed");
             }
 
-            // TODO: i18n
-            String resultMessage = "New token created:<br /><br /><code>" + newApiToken + "</code>";
-            resultMessage += "<br /><br /><small>Please copy it now, it will not be shown again.</small>";
+            String resultMessage = getLocalizedMessage("auth.createapitoken.success") + "<br /><br /><code>"
+                    + newApiToken + "</code>";
+            resultMessage += "<br /><br /><small>" + getLocalizedMessage("auth.createapitoken.success.footer")
+                    + "</small>";
             resp.setContentType("text/html;charset=UTF-8");
-            resp.getWriter().append(getResultPageBody(params, resultMessage)); // TODO: i18n
+            resp.getWriter().append(getResultPageBody(params, resultMessage));
             resp.getWriter().close();
         } catch (AuthenticationException e) {
             processFailedLogin(resp, params, e.getMessage());
@@ -145,8 +145,8 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
 
     @Override
     protected String getPageBody(Map<String, String[]> params, String message, boolean hideForm) {
-        String responseBody = pageTemplate.replace("{form_fields}", getFormFields(params));
-        String buttonLabel = "Create API Token"; // TODO: i18n
+        String responseBody = getPageTemplate().replace("{form_fields}", getFormFields(params));
+        String buttonLabel = getLocalizedMessage("auth.button.createapitoken");
         responseBody = responseBody.replace("{message}", message);
         responseBody = responseBody.replace("{formAction}", "/createApiToken");
         responseBody = responseBody.replace("{formClass}", hideForm ? "hide" : "show");
@@ -160,7 +160,7 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
     }
 
     protected String getResultPageBody(Map<String, String[]> params, String message) {
-        String responseBody = pageTemplate.replace("{form_fields}", "");
+        String responseBody = getPageTemplate().replace("{form_fields}", "");
         responseBody = responseBody.replace("{message}", message);
         responseBody = responseBody.replace("{formAction}", "/createApiToken");
         responseBody = responseBody.replace("{formClass}", "hide");
diff --git a/bundles/org.openhab.core.io.http.auth/src/main/resources/messages.properties b/bundles/org.openhab.core.io.http.auth/src/main/resources/messages.properties
new file mode 100644
index 0000000000..ac450af091
--- /dev/null
+++ b/bundles/org.openhab.core.io.http.auth/src/main/resources/messages.properties
@@ -0,0 +1,26 @@
+auth.login.prompt = Sign in to grant <b>%s</b> access to <b>%s</b>:
+auth.login.fail = Please try again.
+auth.createaccount.prompt = Create a first administrator account to continue.
+
+auth.changepassword.success = Password changed.
+
+auth.createapitoken.prompt = Create a new API token to authorize external services.
+auth.createapitoken.name.unique.fail = A token with the same name already exists, please try again.
+auth.createapitoken.name.format.fail = Invalid token name, please use alphanumeric characters only.
+auth.createapitoken.success = New token created:
+auth.createapitoken.success.footer = Please copy it now, it will not be shown again.
+
+auth.password.confirm.fail = Passwords don't match, please try again.
+
+auth.placeholder.username = User Name
+auth.placeholder.password = Password
+auth.placeholder.newpassword = New Password
+auth.placeholder.repeatpassword = Confirm New Password
+auth.placeholder.tokenname = Token Name
+auth.placeholder.tokenscope = Token Scope (optional)
+
+auth.button.signin = Sign In
+auth.button.createaccount = Create Account
+auth.button.changepassword = Change Password
+auth.button.createapitoken = Create API Token
+auth.button.return = Return Home
diff --git a/bundles/org.openhab.core.io.http.auth/src/main/resources/messages_fr.properties b/bundles/org.openhab.core.io.http.auth/src/main/resources/messages_fr.properties
new file mode 100644
index 0000000000..a0d15bf2dc
--- /dev/null
+++ b/bundles/org.openhab.core.io.http.auth/src/main/resources/messages_fr.properties
@@ -0,0 +1,26 @@
+auth.login.prompt = Connectez-vous pour accorder l'acc�s <b>%s</b> � <b>%s</b>:
+auth.login.fail = Veuillez r�essayer.
+auth.createaccount.prompt = Cr�ez un premier compte administrateur pour continuer.
+
+auth.changepassword.success = Mot de passe modifi�.
+
+auth.createapitoken.prompt = Cr�ez des jetons d'API pour autoriser des services externes.
+auth.createapitoken.name.unique.fail = Un jeton avec le m�me nom existe d�j�, veuillez r�essayer.
+auth.createapitoken.name.format.fail = Nom de jeton invalide, merci d'utiliser uniquement des caract�res alphanum�riques.
+auth.createapitoken.success = Nouveau jeton cr�� :
+auth.createapitoken.success.footer = Veuillez le copier maintenant, il ne sera plus possible de le voir � nouveau.
+
+auth.password.confirm.fail = Les mots de passe ne correspondent pas, veuillez r�essayer.
+
+auth.placeholder.username = Utilisateur
+auth.placeholder.password = Mot de passe
+auth.placeholder.newpassword = Nouveau mot de passe
+auth.placeholder.repeatpassword = Confirmer le nouveau mot de passe
+auth.placeholder.tokenname = Nom du jeton
+auth.placeholder.tokenscope = Port�e (scope) du jeton, facultatif
+
+auth.button.signin = Connexion
+auth.button.createaccount = Cr�er
+auth.button.changepassword = Changer le mot de passe
+auth.button.createapitoken = Cr�er le jeton
+auth.button.return = Retour