#111: translate controllers

This commit is contained in:
Benjamin Gamard 2017-11-01 14:34:15 +01:00
parent f46e10e11c
commit c7b9ec3a4c
17 changed files with 3546 additions and 90 deletions

View File

@ -6,13 +6,13 @@
angular.module('docs',
// Dependencies
['ui.router', 'ui.route', 'ui.bootstrap', 'ui.keypress', 'ui.validate', 'dialog', 'ngProgress', 'monospaced.qrcode',
'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'angularFileUpload']
'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'angularFileUpload', 'pascalprecht.translate']
)
/**
* Configuring modules.
*/
.config(function($stateProvider, $httpProvider, RestangularProvider) {
.config(function($stateProvider, $httpProvider, RestangularProvider, $translateProvider) {
// Configuring UI Router
$stateProvider
.state('main', {
@ -334,9 +334,25 @@ angular.module('docs',
}
}
});
// Configuring Restangular
RestangularProvider.setBaseUrl('../api');
// Configuring Angular Translate
$translateProvider
.useSanitizeValueStrategy(null)
.useStaticFilesLoader({
prefix: 'locale/',
suffix: '.json'
})
.registerAvailableLanguageKeys(['en', 'fr'], {
'en_*': 'en',
'fr_*': 'fr',
'*': 'en'
})
.determinePreferredLanguage()
.fallbackLanguage('en');
// Configuring $http to act like jQuery.ajax
$httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';
$httpProvider.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';

View File

@ -3,7 +3,7 @@
/**
* Login controller.
*/
angular.module('docs').controller('Login', function(Restangular, $scope, $rootScope, $state, $dialog, User) {
angular.module('docs').controller('Login', function(Restangular, $scope, $rootScope, $state, $dialog, User, $translate) {
$scope.codeRequired = false;
// Get the app configuration
@ -33,9 +33,9 @@ angular.module('docs').controller('Login', function(Restangular, $scope, $rootSc
$scope.codeRequired = true;
} else {
// Login truly failed
var title = 'Login failed';
var msg = 'Username or password invalid';
var btns = [{result: 'ok', label: 'OK', cssClass: 'btn-primary'}];
var title = $translate.instant('login.login_failed_title');
var msg = $translate.instant('login.login_failed_message');
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns);
}
});

View File

@ -3,7 +3,7 @@
/**
* Document default controller.
*/
angular.module('docs').controller('DocumentDefault', function($scope, $rootScope, $state, Restangular, $upload) {
angular.module('docs').controller('DocumentDefault', function($scope, $rootScope, $state, Restangular, $upload, $translate) {
// Load app data
Restangular.one('app').get().then(function(data) {
$scope.app = data;
@ -39,7 +39,7 @@ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope
name: file.name,
create_date: new Date().getTime(),
mimetype: file.type,
status: 'Pending...'
status: $translate.instant('document.default.upload_pending')
};
$scope.files.push(newfile);
newfiles.push(newfile);
@ -63,7 +63,7 @@ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope
*/
$scope.uploadFile = function(file, newfile) {
// Upload the file
newfile.status = 'Uploading...';
newfile.status = $translate.instant('document.default.upload_progress');
return $upload.upload({
method: 'PUT',
url: '../api/file',
@ -81,9 +81,9 @@ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope
$rootScope.userInfo.storage_current += data.size;
})
.error(function (data) {
newfile.status = 'Upload error';
newfile.status = $translate.instant('document.default.upload_error');
if (data.type == 'QuotaReached') {
newfile.status += ' - Quota reached';
newfile.status += ' - ' + $translate.instant('document.default.upload_error_quota');
}
});
};

View File

@ -94,7 +94,6 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
// Upload files after edition
promise.then(function(data) {
console.log('document created, adding file', $scope.newFiles);
$scope.fileProgress = 0;
// When all files upload are over, attach orphan files and move on
@ -122,7 +121,7 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
navigateNext();
} else {
$scope.fileIsUploading = true;
$rootScope.pageTitle = '0% - Sismics Docs';
$rootScope.pageTitle = '0% - ' + $rootScope.appName;
// Send a file from the input file array and return a promise
var sendFile = function(key) {
@ -134,7 +133,6 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
};
// Build the payload
console.log('sending file', key, $scope.newFiles[key], data);
var file = $scope.newFiles[key];
var formData = new FormData();
formData.append('id', data.id);
@ -149,7 +147,6 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
contentType: false,
processData: false,
success: function(response) {
console.log('file uploaded successfully', formData);
deferred.resolve(response);
},
error: function(jqXHR) {
@ -172,19 +169,19 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
// Error uploading a file, we stop here
$scope.alerts.unshift({
type: 'danger',
msg: 'Document successfully ' + ($scope.isEdit() ? 'edited' : 'added') + ' but some files cannot be uploaded'
+ (data.responseJSON.type == 'QuotaReached' ? ' - Quota reached' : '')
msg: $translate.instant('document.edit.document_' + ($scope.isEdit() ? 'edited' : 'added') + '_with_errors')
+ (data.responseJSON.type == 'QuotaReached' ? (' - ' + $translate.instant('document.edit.quota_reached')) : '')
});
// Reset view and title
$scope.fileIsUploading = false;
$scope.fileProgress = 0;
$rootScope.pageTitle = 'Sismics Docs';
$rootScope.pageTitle = $rootScope.appName;
}, function(e) {
var done = 1 - (e.total - e.loaded) / e.total;
var chunk = 100 / _.size($scope.newFiles);
$scope.fileProgress = startProgress + done * chunk;
$rootScope.pageTitle = Math.round($scope.fileProgress) + '% - Sismics Docs';
$rootScope.pageTitle = Math.round($scope.fileProgress) + '% - ' + $rootScope.appName;
});
return deferred.promise;
@ -195,13 +192,11 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
var then = function() {
key++;
if ($scope.newFiles[key]) {
console.log('sending new file');
sendFile(key).then(then);
} else {
$scope.fileIsUploading = false;
$scope.fileProgress = 0;
$rootScope.pageTitle = 'Sismics Docs';
console.log('finished sending files, bye');
$rootScope.pageTitle = $rootScope.appName;
navigateNext();
}
};

View File

@ -3,7 +3,7 @@
/**
* Document view controller.
*/
angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $modal, Restangular, $timeout) {
angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $modal, Restangular, $translate) {
// Load document data from server
Restangular.one('document', $stateParams.id).get().then(function(data) {
$scope.document = data;
@ -40,11 +40,11 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
* Delete a comment.
*/
$scope.deleteComment = function(comment) {
var title = 'Delete comment';
var msg = 'Do you really want to delete this comment?';
var title = $translate.instant('document.view.delete_comment_title');
var msg = $translate.instant('document.view.delete_comment_message');
var btns = [
{result: 'cancel', label: 'Cancel'},
{result: 'ok', label: 'OK', cssClass: 'btn-primary'}
{result: 'cancel', label: $translate.instant('cancel')},
{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}
];
$dialog.messageBox(title, msg, btns, function (result) {
@ -60,11 +60,11 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
* Delete a document.
*/
$scope.deleteDocument = function (document) {
var title = 'Delete document';
var msg = 'Do you really want to delete this document?';
var title = $translate.instant('document.view.delete_document_title');
var msg = $translate.instant('document.view.delete_document_message');
var btns = [
{result: 'cancel', label: 'Cancel'},
{result: 'ok', label: 'OK', cssClass: 'btn-primary'}
{result: 'cancel', label: $translate.instant('cancel')},
{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}
];
$dialog.messageBox(title, msg, btns, function (result) {
@ -108,14 +108,11 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
$scope.showShare = function(share) {
// Show the link
var link = $location.absUrl().replace($location.path(), '').replace('#', '') + 'share.html#/share/' + $stateParams.id + '/' + share.id;
var title = 'Shared document';
var msg = 'You can share this document by giving this link. ' +
'Note that everyone having this link can see the document.<br/>' +
'<input class="form-control share-link" type="text" readonly="readonly" value="' + link + '"' +
' onclick="this.select(); document.execCommand(\'copy\');" />';
var title = $translate.instant('document.view.shared_document_title');
var msg = $translate.instant('document.view.shared_document_message', { link: link });
var btns = [
{result: 'unshare', label: 'Unshare', cssClass: 'btn-danger'},
{result: 'close', label: 'Close'}
{result: 'unshare', label: $translate.instant('unshare'), cssClass: 'btn-danger'},
{result: 'close', label: $translate.instant('close')}
];
$dialog.messageBox(title, msg, btns, function (result) {

View File

@ -3,7 +3,7 @@
/**
* Document view content controller.
*/
angular.module('docs').controller('DocumentViewContent', function ($scope, $rootScope, $stateParams, Restangular, $dialog, $state, $upload) {
angular.module('docs').controller('DocumentViewContent', function ($scope, $rootScope, $stateParams, Restangular, $dialog, $state, $upload, $translate) {
/**
* Configuration for file sorting.
*/
@ -45,11 +45,11 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
* Delete a file.
*/
$scope.deleteFile = function (file) {
var title = 'Delete file';
var msg = 'Do you really want to delete this file?';
var title = $translate.instant('document.view.content.delete_file_title');
var msg = $translate.instant('document.view.content.delete_file_message');
var btns = [
{result: 'cancel', label: 'Cancel'},
{result: 'ok', label: 'OK', cssClass: 'btn-primary'}
{result: 'cancel', label: $translate.instant('cancel')},
{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}
];
$dialog.messageBox(title, msg, btns, function (result) {
@ -82,7 +82,7 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
name: file.name,
create_date: new Date().getTime(),
mimetype: file.type,
status: 'Pending...'
status: $translate.instant('document.view.content.upload_pending')
};
$scope.files.push(newfile);
newfiles.push(newfile);
@ -104,7 +104,7 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
*/
$scope.uploadFile = function(file, newfile) {
// Upload the file
newfile.status = 'Uploading...';
newfile.status = $translate.instant('document.view.content.upload_progress');
return $upload.upload({
method: 'PUT',
url: '../api/file',
@ -125,9 +125,9 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
$rootScope.userInfo.storage_current += data.size;
})
.error(function (data) {
newfile.status = 'Upload error';
newfile.status = $translate.instant('document.view.content.upload_error');
if (data.type == 'QuotaReached') {
newfile.status += ' - Quota reached';
newfile.status += ' - ' + $translate.instant('document.view.content.upload_error_quota');
}
});
};

View File

@ -3,7 +3,7 @@
/**
* Settings account controller.
*/
angular.module('docs').controller('SettingsAccount', function($scope, Restangular) {
angular.module('docs').controller('SettingsAccount', function($scope, Restangular, $translate) {
$scope.editUserAlert = false;
// Alerts
@ -22,7 +22,7 @@ angular.module('docs').controller('SettingsAccount', function($scope, Restangula
$scope.editUser = function() {
Restangular.one('user').post('', $scope.user).then(function() {
$scope.user = {};
$scope.alerts.push({ type: 'success', msg: 'Account successfully updated' });
$scope.alerts.push({ type: 'success', msg: $translate.instant('settings.account.updated') });
});
};
});

View File

@ -3,7 +3,7 @@
/**
* Settings group edition page controller.
*/
angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog, $state, $stateParams, Restangular, $q) {
angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog, $state, $stateParams, Restangular, $q, $translate) {
/**
* Returns true if in edit mode (false in add mode).
*/
@ -52,9 +52,12 @@ angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog,
* Delete the current group.
*/
$scope.remove = function() {
var title = 'Delete group';
var msg = 'Do you really want to delete this group?';
var btns = [{result:'cancel', label: 'Cancel'}, {result:'ok', label: 'OK', cssClass: 'btn-primary'}];
var title = $translate.instant('settings.group.edit.delete_group_title');
var msg = $translate.instant('settings.group.edit.delete_group_message');
var btns = [
{ result:'cancel', label: $translate.instant('cancel') },
{ result:'ok', label: $translate.instant('ok'), cssClass: 'btn-primary' }
];
$dialog.messageBox(title, msg, btns, function(result) {
if (result == 'ok') {

View File

@ -3,7 +3,7 @@
/**
* Settings security controller.
*/
angular.module('docs').controller('SettingsSecurity', function($scope, User, $dialog, $modal, Restangular) {
angular.module('docs').controller('SettingsSecurity', function($scope, User, $dialog, $modal, Restangular, $translate) {
User.userInfo().then(function(data) {
$scope.user = data;
});
@ -12,9 +12,12 @@ angular.module('docs').controller('SettingsSecurity', function($scope, User, $di
* Enable TOTP.
*/
$scope.enableTotp = function() {
var title = 'Enable two-factor authentication';
var msg = 'Make sure you have a TOTP-compatible application on your phone ready to add a new account';
var btns = [{result:'cancel', label: 'Cancel'}, {result:'ok', label: 'OK', cssClass: 'btn-primary'}];
var title = $translate.instant('settings.security.enable_totp_title');
var msg = $translate.instant('settings.security.enable_totp_message');
var btns = [
{ result:'cancel', label: $translate.instant('cancel') },
{ result:'ok', label: $translate.instant('ok'), cssClass: 'btn-primary' }
];
$dialog.messageBox(title, msg, btns, function(result) {
if (result == 'ok') {

View File

@ -3,7 +3,7 @@
/**
* Settings user edition page controller.
*/
angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog, $state, $stateParams, Restangular) {
angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog, $state, $stateParams, Restangular, $translate) {
/**
* Returns true if in edit mode (false in add mode).
*/
@ -49,9 +49,12 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog,
* Delete the current user.
*/
$scope.remove = function() {
var title = 'Delete user';
var msg = 'Do you really want to delete this user? All associated documents, files and tags will be deleted';
var btns = [{result:'cancel', label: 'Cancel'}, {result:'ok', label: 'OK', cssClass: 'btn-primary'}];
var title = $translate.instant('settings.user.edit.delete_user_title');
var msg = $translate.instant('settings.user.edit.delete_user_message');
var btns = [
{ result:'cancel', label: $translate.instant('cancel') },
{ result:'ok', label: $translate.instant('ok'), cssClass: 'btn-primary' }
];
$dialog.messageBox(title, msg, btns, function(result) {
if (result == 'ok') {

View File

@ -3,7 +3,7 @@
/**
* Tag edit controller.
*/
angular.module('docs').controller('TagEdit', function($scope, $stateParams, Restangular, $dialog, $state) {
angular.module('docs').controller('TagEdit', function($scope, $stateParams, Restangular, $dialog, $state, $translate) {
// Retrieve the tag
Restangular.one('tag', $stateParams.id).get().then(function(data) {
$scope.tag = data;
@ -28,11 +28,11 @@ angular.module('docs').controller('TagEdit', function($scope, $stateParams, Rest
* Delete a tag.
*/
$scope.deleteTag = function(tag) {
var title = 'Delete tag';
var msg = 'Do you really want to delete this tag?';
var title = $translate.instant('tag.edit.delete_tag_title');
var msg = $translate.instant('tag.edit.delete_tag_message');
var btns = [
{result: 'cancel', label: 'Cancel'},
{result: 'ok', label: 'OK', cssClass: 'btn-primary'}
{result: 'cancel', label: $translate.instant('cancel')},
{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}
];
$dialog.messageBox(title, msg, btns, function(result) {

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html ng-app="docs">
<head>
<title ng-bind-template="{{ appName }}">Sismics Docs</title>
<title ng-bind-template="{{ pageTitle ? pageTitle : appName }}">Sismics Docs</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="../api/theme/image/logo" />
@ -29,6 +29,7 @@
<script src="lib/underscore.js" type="text/javascript"></script>
<script src="lib/colorpicker.js" type="text/javascript"></script>
<script src="lib/angular.js" type="text/javascript"></script>
<script src="lib/angular.translate.js" type="text/javascript"></script>
<script src="lib/angular.sanitize.js" type="text/javascript"></script>
<script src="lib/angular.touch.js" type="text/javascript"></script>
<script src="lib/angular.ui-router.js" type="text/javascript"></script>
@ -92,7 +93,7 @@
<button type="button" class="navbar-toggle"
ng-init="isCollapsed = true"
ng-click="isCollapsed = !isCollapsed">
<span class="sr-only">Toggle navigation</span>
<span class="sr-only">{{ 'index.toggle_navigation' | translate }}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
@ -108,37 +109,37 @@
<div class="collapse navbar-collapse" collapse="isCollapsed">
<ul class="nav navbar-nav" ng-show="!userInfo.anonymous">
<li ng-class="{active: $uiRoute}" ui-route="/document.*">
<a href="#/document"><span class="glyphicon glyphicon-book"></span> Documents</a>
<a href="#/document"><span class="glyphicon glyphicon-book"></span> {{ 'index.nav_documents' | translate }}</a>
</li>
<li ng-class="{active: $uiRoute}" ui-route="/tag.*">
<a href="#/tag"><span class="glyphicon glyphicon-tags"></span> Tags</a>
<a href="#/tag"><span class="glyphicon glyphicon-tags"></span> {{ 'index.nav_tags' | translate }}</a>
</li>
<li ng-class="{active: $uiRoute}" ui-route="/user.*">
<a href="#/user"><span class="glyphicon glyphicon-user"></span> Users &amp; Groups</a>
<a href="#/user"><span class="glyphicon glyphicon-user"></span> {{ 'index.nav_users_groups' | translate }}</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right" ng-show="!userInfo.anonymous">
<li ng-show="errorNumber > 0">
<a href="#/settings/log" ng-click="openLogs()" class="nav-text-error">
<span class="glyphicon glyphicon-warning-sign"></span> {{ errorNumber }} new error{{ errorNumber > 1 ? 's' : '' }}
<span class="glyphicon glyphicon-warning-sign"></span> {{ 'index.error_info' | translate: '{ count: errorNumber }' }}
</a>
</li>
<li>
<a href="{{ userInfo.username == 'guest' ? '#/user/guest' : '#/settings/account' }}"
title="Logged in as {{ userInfo.username }}">
title="{{ 'index.logged_as' | translate: '{ username: userInfo.username }' }}">
<span class="glyphicon glyphicon-user"></span>
{{ userInfo.username }}
</a>
</li>
<li ng-class="{active: $uiRoute}" ui-route="/settings.*" ng-show="userInfo.username != 'guest'">
<a href="#/settings/account">
<span class="glyphicon glyphicon-cog"></span> Settings
<span class="glyphicon glyphicon-cog"></span> {{ 'index.nav_settings' | translate }}
</a>
</li>
<li>
<a href="#" ng-click="logout($event)">
<span class="glyphicon glyphicon-off"></span> Logout
<span class="glyphicon glyphicon-off"></span> {{ 'index.logout' | translate }}
</a>
</li>
</ul>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,84 @@
{
"login": {
"username": "Username",
"password": "Password",
"validation_code_required": "A validation code is required",
"validation_code_title": "You have activated the two-factor authentication on your account. Please enter a validation code generated by the phone app your configured.",
"validation_code": "Validation code",
"remember_me": "Remember me",
"submit": "Sign in",
"login_as_guest": "Login as guest",
"login_failed_title": "Login failed",
"login_failed_message": "Username or password invalid"
},
"index": {
"toggle_navigation": "Toggle navigation",
"nav_documents": "Documents",
"nav_tags": "Tags",
"nav_users_groups": "Users & Groups",
"error_info": "{{ count }} new error{{ count > 1 ? 's' : '' }}",
"logged_as": "Logged in as {{ username }}",
"nav_settings": "Settings",
"logout": "Logout"
},
"document": {
"view": {
"content": {
"delete_file_title": "Delete file",
"delete_file_message": "Do you really want to delete this file?",
"upload_pending": "Pending...",
"upload_progress": "Uploading...",
"upload_error": "Upload error",
"upload_error_quota": "Quota reached"
},
"delete_comment_title": "Delete comment",
"delete_comment_message": "Do you really want to delete this comment?",
"delete_document_title": "Delete document",
"delete_document_message": "Do you really want to delete this document?",
"shared_document_title": "Shared document",
"shared_document_message": "You can share this document by giving this link. Note that everyone having this link can see the document.<br/><input class=\"form-control share-link\" type=\"text\" readonly=\"readonly\" value=\"{{ link }}\" onclick=\"this.select(); document.execCommand('copy');\" />"
},
"edit": {
"document_edited_with_errors": "Document successfully edited but some files cannot be uploaded",
"document_added_with_errors": "Document successfully added but some files cannot be uploaded",
"quota_reached": "Quota reached"
},
"default": {
"upload_pending": "Pending...",
"upload_progress": "Uploading...",
"upload_error": "Upload error",
"upload_error_quota": "Quota reached"
}
},
"tag": {
"edit": {
"delete_tag_title": "Delete tag",
"delete_tag_message": "Do you really want to delete this tag?"
}
},
"settings": {
"user": {
"edit": {
"delete_user_title": "Delete user",
"delete_user_message": "Do you really want to delete this user? All associated documents, files and tags will be deleted"
}
},
"security": {
"enable_totp_title": "Enable two-factor authentication",
"enable_totp_message": "Make sure you have a TOTP-compatible application on your phone ready to add a new account"
},
"group": {
"edit": {
"delete_group_title": "Delete group",
"delete_group_message": "Do you really want to delete this group?"
}
},
"account": {
"updated": "Account successfully updated"
}
},
"ok": "OK",
"cancel": "Cancel",
"unshare": "Unshare",
"close": "Close"
}

View File

@ -0,0 +1,2 @@
{
}

View File

@ -1,3 +0,0 @@
{
"app": "Sismics Docs"
}

View File

@ -8,38 +8,38 @@
<div class="col-sm-offset-5 col-sm-2 login-box">
<form>
<div class="form-group">
<label class="sr-only" for="inputUsername">Username</label>
<input class="form-control" type="text" id="inputUsername" placeholder="Username" ng-model="user.username" />
<label class="sr-only" for="inputUsername">{{ 'login.username' | translate }}</label>
<input class="form-control" type="text" id="inputUsername" placeholder="{{ 'login.username' | translate }}" ng-model="user.username" />
</div>
<div class="form-group">
<label class="sr-only" for="inputPassword">Password</label>
<input class="form-control" type="password" id="inputPassword" placeholder="Password" ng-model="user.password" />
<label class="sr-only" for="inputPassword">{{ 'login.password' | translate }}</label>
<input class="form-control" type="password" id="inputPassword" placeholder="{{ 'login.password' | translate }}" ng-model="user.password" />
</div>
<span class="help-block" ng-if="codeRequired">
A validation code is required
<span class="glyphicon glyphicon-question-sign" title="You have activated the two-factor authentication on your account. Please enter a validation code generated by the phone app your configured."></span>
{{ 'login.validation_code_required' | translate }}
<span class="glyphicon glyphicon-question-sign" title="{{ 'login.validation_code_title' | translate }}"></span>
</span>
<div class="form-group" ng-if="codeRequired">
<label class="sr-only" for="inputCode">Validation code</label>
<input class="form-control" type="text" id="inputCode" placeholder="Validation code" ng-model="user.code" />
<label class="sr-only" for="inputCode">{{ 'login.validation_code' | translate }}</label>
<input class="form-control" type="text" id="inputCode" placeholder="{{ 'login.validation_code' | translate }}" ng-model="user.code" />
</div>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="user.remember" /> Remember me
<input type="checkbox" ng-model="user.remember" /> {{ 'login.remember_me' | translate }}
</label>
</div>
<button type="submit" class="btn btn-primary btn-block" ng-click="login()">
<span class="glyphicon glyphicon-ok"></span> Sign in
<span class="glyphicon glyphicon-ok"></span> {{ 'login.submit' | translate }}
</button>
<p class="text-center lead" ng-if="app.guest_login">&nbsp;</p>
<button type="submit" class="btn btn-default btn-block" ng-if="app.guest_login" ng-click="loginAsGuest()">
<span class="glyphicon glyphicon-user"></span> Login as guest
<span class="glyphicon glyphicon-user"></span> {{ 'login.login_as_guest' | translate }}
</button>
</form>
</div>