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>
This commit is contained in:
Yannick Schaus 2020-12-12 22:42:56 +01:00 committed by GitHub
parent e909a81f4b
commit 67bdfa3ad6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 127 additions and 41 deletions

View File

@ -6,6 +6,7 @@
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </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"> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes> <attributes>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>

View File

@ -102,29 +102,29 @@ input.submit:hover {
<form class="{formClass}" method="POST" action="{formAction}"> <form class="{formClass}" method="POST" action="{formAction}">
{form_fields} {form_fields}
<div> <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>
<div> <div>
<input class="field" type="password" placeholder="Password" name="password" required /> <input class="field" type="password" placeholder="{passwordPlaceholder}" name="password" required />
</div> </div>
<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>
<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>
<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>
<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>
<div> <div>
<input class="submit" type="Submit" value="{buttonLabel}" /> <input class="submit" type="Submit" value="{buttonLabel}" />
</div> </div>
</form> </form>
<div class="result{resultClass}"> <div class="result{resultClass}">
<a class="submit" type="button" href="/">Return Home</a> <a class="submit" type="button" href="/">{returnButtonLabel}</a>
</div> </div>
</body> </body>
</html> </html>

View File

@ -21,6 +21,8 @@ import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;
import java.util.UUID; import java.util.UUID;
import javax.servlet.http.HttpServlet; 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.User;
import org.openhab.core.auth.UserRegistry; import org.openhab.core.auth.UserRegistry;
import org.openhab.core.auth.UsernamePasswordCredentials; import org.openhab.core.auth.UsernamePasswordCredentials;
import org.openhab.core.i18n.LocaleProvider;
import org.osgi.framework.BundleContext; import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService; import org.osgi.service.http.HttpService;
@ -51,11 +54,14 @@ public abstract class AbstractAuthPageServlet extends HttpServlet {
protected static final long serialVersionUID = 5340598701104679840L; protected static final long serialVersionUID = 5340598701104679840L;
private static final String MESSAGES_BUNDLE_NAME = "messages";
private final Logger logger = LoggerFactory.getLogger(AbstractAuthPageServlet.class); private final Logger logger = LoggerFactory.getLogger(AbstractAuthPageServlet.class);
protected HttpService httpService; protected HttpService httpService;
protected UserRegistry userRegistry; protected UserRegistry userRegistry;
protected AuthenticationProvider authProvider; protected AuthenticationProvider authProvider;
protected LocaleProvider localeProvider;
protected @Nullable Instant lastAuthenticationFailure; protected @Nullable Instant lastAuthenticationFailure;
protected int authenticationFailureCount = 0; protected int authenticationFailureCount = 0;
@ -64,10 +70,12 @@ public abstract class AbstractAuthPageServlet extends HttpServlet {
protected String pageTemplate; protected String pageTemplate;
public AbstractAuthPageServlet(BundleContext bundleContext, @Reference HttpService httpService, 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.httpService = httpService;
this.userRegistry = userRegistry; this.userRegistry = userRegistry;
this.authProvider = authProvider; this.authProvider = authProvider;
this.localeProvider = localeProvider;
pageTemplate = ""; pageTemplate = "";
URL resource = bundleContext.getBundle().getResource("pages/authorize.html"); 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 getPageBody(Map<String, String[]> params, String message, boolean hideForm);
protected abstract String getFormFields(Map<String, String[]> params); protected abstract String getFormFields(Map<String, String[]> params);
@ -123,7 +148,13 @@ public abstract class AbstractAuthPageServlet extends HttpServlet {
authenticationFailureCount += 1; authenticationFailureCount += 1;
resp.setContentType("text/html;charset=UTF-8"); resp.setContentType("text/html;charset=UTF-8");
logger.warn("Authentication failed: {}", message); 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(); 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);
}
} }

View File

@ -32,6 +32,7 @@ import org.openhab.core.auth.PendingToken;
import org.openhab.core.auth.Role; import org.openhab.core.auth.Role;
import org.openhab.core.auth.User; import org.openhab.core.auth.User;
import org.openhab.core.auth.UserRegistry; import org.openhab.core.auth.UserRegistry;
import org.openhab.core.i18n.LocaleProvider;
import org.osgi.framework.BundleContext; import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
@ -63,8 +64,9 @@ public class AuthorizePageServlet extends AbstractAuthPageServlet {
@Activate @Activate
public AuthorizePageServlet(BundleContext bundleContext, @Reference HttpService httpService, public AuthorizePageServlet(BundleContext bundleContext, @Reference HttpService httpService,
@Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider) { @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider,
super(bundleContext, httpService, userRegistry, authProvider); @Reference LocaleProvider localeProvider) {
super(bundleContext, httpService, userRegistry, authProvider, localeProvider);
try { try {
httpService.registerServlet("/auth", this, null, null); httpService.registerServlet("/auth", this, null, null);
} catch (NamespaceException | ServletException e) { } catch (NamespaceException | ServletException e) {
@ -86,11 +88,10 @@ public class AuthorizePageServlet extends AbstractAuthPageServlet {
throw new IllegalArgumentException("invalid_request"); throw new IllegalArgumentException("invalid_request");
} }
// TODO: i18n
if (isSignupMode()) { if (isSignupMode()) {
message = "Create a first administrator account to continue."; message = getLocalizedMessage("auth.createaccount.prompt");
} else { } 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.setContentType("text/html;charset=UTF-8");
resp.getWriter().append(getPageBody(params, message, false)); 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 // first verify the password confirmation and bail out if necessary
if (!params.containsKey("password_repeat") || !password.equals(params.get("password_repeat")[0])) { if (!params.containsKey("password_repeat") || !password.equals(params.get("password_repeat")[0])) {
resp.setContentType("text/html;charset=UTF-8"); resp.setContentType("text/html;charset=UTF-8");
// TODO: i18n resp.getWriter()
resp.getWriter().append(getPageBody(params, "Passwords don't match, please try again.", false)); .append(getPageBody(params, getLocalizedMessage("auth.password.confirm.fail"), false));
resp.getWriter().close(); resp.getWriter().close();
return; return;
} }
@ -202,9 +203,9 @@ public class AuthorizePageServlet extends AbstractAuthPageServlet {
@Override @Override
protected String getPageBody(Map<String, String[]> params, String message, boolean hideForm) { 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 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("{message}", message);
responseBody = responseBody.replace("{formAction}", "/auth"); responseBody = responseBody.replace("{formAction}", "/auth");
responseBody = responseBody.replace("{formClass}", "show"); responseBody = responseBody.replace("{formClass}", "show");

View File

@ -25,6 +25,7 @@ import org.openhab.core.auth.AuthenticationProvider;
import org.openhab.core.auth.ManagedUser; import org.openhab.core.auth.ManagedUser;
import org.openhab.core.auth.User; import org.openhab.core.auth.User;
import org.openhab.core.auth.UserRegistry; import org.openhab.core.auth.UserRegistry;
import org.openhab.core.i18n.LocaleProvider;
import org.osgi.framework.BundleContext; import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
@ -51,8 +52,9 @@ public class ChangePasswordPageServlet extends AbstractAuthPageServlet {
@Activate @Activate
public ChangePasswordPageServlet(BundleContext bundleContext, @Reference HttpService httpService, public ChangePasswordPageServlet(BundleContext bundleContext, @Reference HttpService httpService,
@Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider) { @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider,
super(bundleContext, httpService, userRegistry, authProvider); @Reference LocaleProvider localeProvider) {
super(bundleContext, httpService, userRegistry, authProvider, localeProvider);
try { try {
httpService.registerServlet("/changePassword", this, null, null); httpService.registerServlet("/changePassword", this, null, null);
} catch (NamespaceException | ServletException e) { } 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])) { if (!params.containsKey("password_repeat") || !newPassword.equals(params.get("password_repeat")[0])) {
resp.setContentType("text/html;charset=UTF-8"); resp.setContentType("text/html;charset=UTF-8");
// TODO: i18n resp.getWriter().append(getPageBody(params, getLocalizedMessage("auth.password.confirm.fail"), false));
resp.getWriter().append(getPageBody(params, "Passwords don't match, please try again.", false));
resp.getWriter().close(); resp.getWriter().close();
return; return;
} }
@ -117,7 +118,7 @@ public class ChangePasswordPageServlet extends AbstractAuthPageServlet {
} }
resp.setContentType("text/html;charset=UTF-8"); 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(); resp.getWriter().close();
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
processFailedLogin(resp, params, e.getMessage()); processFailedLogin(resp, params, e.getMessage());
@ -126,8 +127,8 @@ public class ChangePasswordPageServlet extends AbstractAuthPageServlet {
@Override @Override
protected String getPageBody(Map<String, String[]> params, String message, boolean hideForm) { 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 buttonLabel = "Change Password"; // TODO: i18n String buttonLabel = getLocalizedMessage("auth.button.changepassword");
responseBody = responseBody.replace("{message}", message); responseBody = responseBody.replace("{message}", message);
responseBody = responseBody.replace("{formAction}", "/changePassword"); responseBody = responseBody.replace("{formAction}", "/changePassword");
responseBody = responseBody.replace("{formClass}", hideForm ? "hide" : "show"); 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) { 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("{message}", message);
responseBody = responseBody.replace("{formAction}", "/changePassword"); responseBody = responseBody.replace("{formAction}", "/changePassword");
responseBody = responseBody.replace("{formClass}", "hide"); responseBody = responseBody.replace("{formClass}", "hide");

View File

@ -25,6 +25,7 @@ import org.openhab.core.auth.AuthenticationProvider;
import org.openhab.core.auth.ManagedUser; import org.openhab.core.auth.ManagedUser;
import org.openhab.core.auth.User; import org.openhab.core.auth.User;
import org.openhab.core.auth.UserRegistry; import org.openhab.core.auth.UserRegistry;
import org.openhab.core.i18n.LocaleProvider;
import org.osgi.framework.BundleContext; import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
@ -51,8 +52,9 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
@Activate @Activate
public CreateAPITokenPageServlet(BundleContext bundleContext, @Reference HttpService httpService, public CreateAPITokenPageServlet(BundleContext bundleContext, @Reference HttpService httpService,
@Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider) { @Reference UserRegistry userRegistry, @Reference AuthenticationProvider authProvider,
super(bundleContext, httpService, userRegistry, authProvider); @Reference LocaleProvider localeProvider) {
super(bundleContext, httpService, userRegistry, authProvider, localeProvider);
try { try {
httpService.registerServlet("/createApiToken", this, null, null); httpService.registerServlet("/createApiToken", this, null, null);
} catch (NamespaceException | ServletException e) { } catch (NamespaceException | ServletException e) {
@ -65,9 +67,8 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
Map<String, String[]> params = req.getParameterMap(); Map<String, String[]> params = req.getParameterMap();
try { 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.setContentType("text/html;charset=UTF-8");
resp.getWriter().append(getPageBody(params, message, false)); resp.getWriter().append(getPageBody(params, message, false));
resp.getWriter().close(); resp.getWriter().close();
@ -112,18 +113,16 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
if (((ManagedUser) user).getApiTokens().stream() if (((ManagedUser) user).getApiTokens().stream()
.anyMatch(apiToken -> apiToken.getName().equals(tokenName))) { .anyMatch(apiToken -> apiToken.getName().equals(tokenName))) {
resp.setContentType("text/html;charset=UTF-8"); resp.setContentType("text/html;charset=UTF-8");
// TODO: i18n
resp.getWriter().append( 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(); resp.getWriter().close();
return; return;
} }
if (!tokenName.matches("[a-zA-Z0-9]*")) { if (!tokenName.matches("[a-zA-Z0-9]*")) {
resp.setContentType("text/html;charset=UTF-8"); resp.setContentType("text/html;charset=UTF-8");
// TODO: i18n
resp.getWriter().append( 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(); resp.getWriter().close();
return; return;
} }
@ -132,11 +131,12 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
throw new AuthenticationException("User is not managed"); throw new AuthenticationException("User is not managed");
} }
// TODO: i18n String resultMessage = getLocalizedMessage("auth.createapitoken.success") + "<br /><br /><code>"
String resultMessage = "New token created:<br /><br /><code>" + newApiToken + "</code>"; + newApiToken + "</code>";
resultMessage += "<br /><br /><small>Please copy it now, it will not be shown again.</small>"; resultMessage += "<br /><br /><small>" + getLocalizedMessage("auth.createapitoken.success.footer")
+ "</small>";
resp.setContentType("text/html;charset=UTF-8"); resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().append(getResultPageBody(params, resultMessage)); // TODO: i18n resp.getWriter().append(getResultPageBody(params, resultMessage));
resp.getWriter().close(); resp.getWriter().close();
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
processFailedLogin(resp, params, e.getMessage()); processFailedLogin(resp, params, e.getMessage());
@ -145,8 +145,8 @@ public class CreateAPITokenPageServlet extends AbstractAuthPageServlet {
@Override @Override
protected String getPageBody(Map<String, String[]> params, String message, boolean hideForm) { 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 buttonLabel = "Create API Token"; // TODO: i18n String buttonLabel = getLocalizedMessage("auth.button.createapitoken");
responseBody = responseBody.replace("{message}", message); responseBody = responseBody.replace("{message}", message);
responseBody = responseBody.replace("{formAction}", "/createApiToken"); responseBody = responseBody.replace("{formAction}", "/createApiToken");
responseBody = responseBody.replace("{formClass}", hideForm ? "hide" : "show"); 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) { 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("{message}", message);
responseBody = responseBody.replace("{formAction}", "/createApiToken"); responseBody = responseBody.replace("{formAction}", "/createApiToken");
responseBody = responseBody.replace("{formClass}", "hide"); responseBody = responseBody.replace("{formClass}", "hide");

View File

@ -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

View File

@ -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