Commit d1ed1734 authored by Björn Eyselein's avatar Björn Eyselein

add i18n

parent 09117bfc
Pipeline #26949 passed with stages
in 3 minutes and 53 seconds
version = 2.7.0
version = 2.7.4
align = more
maxColumn = 120
......
......@@ -4,14 +4,14 @@ import java.util.UUID
import com.github.t3hnar.bcrypt._
import javax.inject.{Inject, Singleton}
import model._
import model.graphql.{GraphQLModel, GraphQLRequest}
import model.lti.BasicLtiLaunchRequest
import model.{JsonProtocols, LoggedInUser, LoggedInUserWithToken, User, UserCredentials}
import pdi.jwt.JwtSession
import play.api.{Configuration, Logger}
import play.api.http.HttpErrorHandler
import play.api.libs.json._
import play.api.mvc._
import play.api.{Configuration, Logger}
import play.modules.reactivemongo.{MongoController, ReactiveMongoApi, ReactiveMongoComponents}
import sangria.execution.{ErrorWithResolver, Executor, QueryAnalysisError}
import sangria.marshalling.playJson._
......@@ -36,109 +36,106 @@ class Controller @Inject() (
private val logger = Logger(classOf[Controller])
def index: Action[AnyContent] = assets.at("index.html")
private val apiPrefix = configuration.get[String]("apiPrefix")
def graphiql: Action[AnyContent] =
Action { implicit request =>
Ok(views.html.graphiql())
}
def index: Action[AnyContent] = Action { implicit request =>
Redirect(routes.Controller.langIndex("de"))
}
def assetOrDefault(resource: String): Action[AnyContent] =
if (resource.startsWith(configuration.get[String]("apiPrefix"))) {
Action.async(r => errorHandler.onClientError(r, NOT_FOUND, "Not found"))
} else {
if (resource.contains(".")) assets.at(resource) else index
}
def langIndex(lang: String = "de"): Action[AnyContent] = assets.at(s"$lang/index.html")
def graphiql: Action[AnyContent] = Action { implicit request => Ok(views.html.graphiql()) }
def assetOrDefault(lang: String, resource: String): Action[AnyContent] = if (resource.startsWith(apiPrefix)) {
Action.async(r => errorHandler.onClientError(r, NOT_FOUND, "Not found"))
} else {
if (resource.contains(".")) assets.at(s"$lang/$resource") else langIndex(lang)
}
private implicit val graphQLRequestFormat: Format[GraphQLRequest] = Json.format
def graphql: Action[GraphQLRequest] =
Action.async(parse.json[GraphQLRequest]) { implicit request =>
QueryParser.parse(request.body.query) match {
case Failure(error) => Future.successful(BadRequest(Json.obj("error" -> error.getMessage)))
case Success(queryAst) =>
Executor
.execute(
schema,
queryAst,
operationName = request.body.operationName,
variables = request.body.variables.getOrElse(Json.obj())
)
.map(Ok(_))
.recover {
case error: QueryAnalysisError =>
logger.error("There has been a query error", error)
BadRequest(error.resolveError)
case error: ErrorWithResolver => InternalServerError(error.resolveError)
}
}
def graphql: Action[GraphQLRequest] = Action.async(parse.json[GraphQLRequest]) { implicit request =>
QueryParser.parse(request.body.query) match {
case Failure(error) => Future.successful(BadRequest(Json.obj("error" -> error.getMessage)))
case Success(queryAst) =>
Executor
.execute(
schema,
queryAst,
operationName = request.body.operationName,
variables = request.body.variables.getOrElse(Json.obj())
)
.map(Ok(_))
.recover {
case error: QueryAnalysisError =>
logger.error("There has been a query error", error)
BadRequest(error.resolveError)
case error: ErrorWithResolver => InternalServerError(error.resolveError)
}
}
}
// Json Web Token session
private val jwtHashesToClaim: MutableMap[UUID, (JwtSession, LoggedInUser)] = MutableMap.empty
private def getOrCreateUser(username: String): Future[LoggedInUser] =
futureUserByUsername(username)
.flatMap {
case Some(u) => Future(u)
case None =>
val newUser = User(username)
futureInsertUser(newUser).map { _ => newUser }
}
.map { user => LoggedInUser(user.username) }
def ltiHoneypot: Action[AnyContent] =
Action.async { request =>
request.body.asFormUrlEncoded match {
case None => Future(BadRequest("TODO!"))
case Some(data) =>
val basicLtiRequest = BasicLtiLaunchRequest.fromRequest(data)
private def getOrCreateUser(username: String): Future[LoggedInUser] = futureUserByUsername(username)
.flatMap {
case Some(u) => Future(u)
case None =>
val newUser = User(username)
futureInsertUser(newUser).map { _ => newUser }
}
.map { user => LoggedInUser(user.username) }
getOrCreateUser(basicLtiRequest.ltiExt.username) map { user =>
val uuid = UUID.randomUUID()
def ltiHoneypot: Action[AnyContent] = Action.async { request =>
request.body.asFormUrlEncoded match {
case None => Future(BadRequest("TODO!"))
case Some(data) =>
val basicLtiRequest = BasicLtiLaunchRequest.fromRequest(data)
jwtHashesToClaim.put(uuid, (createJwtSession(user), user))
getOrCreateUser(basicLtiRequest.ltiExt.username) map { user =>
val uuid = UUID.randomUUID()
val redirectUrl = s"/lti/${uuid.toString}"
jwtHashesToClaim.put(uuid, (createJwtSession(user), user))
Redirect(redirectUrl).withNewSession
}
}
Redirect(s"/lti/${uuid.toString}").withNewSession
}
}
}
def claimJsonWebToken(uuidStr: String): Action[AnyContent] =
Action { implicit request =>
jwtHashesToClaim.remove(UUID.fromString(uuidStr)) match {
case None => NotFound("")
case Some((jwtSession, user)) =>
val loggedInUserWithToken = LoggedInUserWithToken(user, jwtSession.serialize)
def claimJsonWebToken(uuidStr: String): Action[AnyContent] = Action { implicit request =>
jwtHashesToClaim.remove(UUID.fromString(uuidStr)) match {
case None => NotFound("")
case Some((jwtSession, user)) =>
val loggedInUserWithToken = LoggedInUserWithToken(user, jwtSession.serialize)
Ok(writeJsonWebToken(loggedInUserWithToken))
}
Ok(writeJsonWebToken(loggedInUserWithToken))
}
def apiAuthenticate: Action[UserCredentials] =
Action.async(parse.json[UserCredentials](JsonProtocols.userCredentialsFormat)) { implicit request =>
futureUserByUsername(request.body.username).map {
case None => BadRequest("Invalid username!")
case Some(user) =>
user.pwHash match {
case None => BadRequest("No password found!")
case Some(pwHash) =>
if (request.body.password.isBcryptedBounded(pwHash)) {
val loggedInUser = LoggedInUser(user.username)
Ok(
writeJsonWebToken(
LoggedInUserWithToken(loggedInUser, createJwtSession(loggedInUser).serialize)
)
}
private implicit val userCredentialsFormat: OFormat[UserCredentials] = JsonProtocols.userCredentialsFormat
def apiAuthenticate: Action[UserCredentials] = Action.async(parse.json[UserCredentials]) { implicit request =>
futureUserByUsername(request.body.username).map {
case None => BadRequest("Invalid username!")
case Some(user) =>
user.pwHash match {
case None => BadRequest("No password found!")
case Some(pwHash) =>
if (request.body.password.isBcryptedBounded(pwHash)) {
val loggedInUser = LoggedInUser(user.username)
Ok(
writeJsonWebToken(
LoggedInUserWithToken(loggedInUser, createJwtSession(loggedInUser).serialize)
)
} else {
BadRequest("Password invalid!")
}
}
}
)
} else {
BadRequest("Password invalid!")
}
}
}
}
}
# Client
GET / controllers.Controller.index
GET / controllers.Controller.index
GET /:lang/ controllers.Controller.langIndex(lang: String)
# LTI
POST /api/lti controllers.Controller.ltiHoneypot
GET /api/claimWebToken/:uuidStr controllers.Controller.claimJsonWebToken(uuidStr: String)
POST /api/lti controllers.Controller.ltiHoneypot
GET /api/claimWebToken/:uuidStr controllers.Controller.claimJsonWebToken(uuidStr: String)
# GraphQL
GET /graphiql controllers.Controller.graphiql
POST /api/graphql controllers.Controller.graphql
GET /graphiql controllers.Controller.graphiql
POST /api/graphql controllers.Controller.graphql
# Map static resources from the /public folder to the /assets URL path
GET /*file controllers.Controller.assetOrDefault(file)
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
GET /:lang/*file controllers.Controller.assetOrDefault(lang: String, file: String)
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
......@@ -14,6 +14,12 @@
"root": "",
"sourceRoot": "src",
"prefix": "it4all",
"i18n": {
"sourceLocale": "de",
"locales": {
"en": "src/locales/messages.en.xlf"
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
......@@ -23,6 +29,7 @@
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"localize": true,
"aot": true,
"assets": [
"src/favicon.ico",
......
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="de" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="1451a321cb66a6785cf15b8d0ada9a620071c6d9" datatype="html">
<source>Sprache</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="140d9a7d0795beace6491c45566f6dd295e94999" datatype="html">
<source>Impressum</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
<trans-unit id="c47b6066a02b6434f0cd861e792d9ceebbe59235" datatype="html">
<source>Datenschutz</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="bb694b49d408265c91c62799c2b3a7e3151c824d" datatype="html">
<source>Logout</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="6765b4c916060f6bc42d9bb69e80377dbcb5e4e9" datatype="html">
<source>Login</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">29</context>
</context-group>
</trans-unit>
<trans-unit id="9adf7f973ff81475f3622c2a38a1e69a7dd433af" datatype="html">
<source>Registrieren</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/app.component.html</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="227094aed22780e90303b4157e24f11db9e2c959" datatype="html">
<source>Nutzername</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/user_management/login-form/login-form.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
</trans-unit>
<trans-unit id="2070ebb54f893c60114f49f46d7e1a6f27b66610" datatype="html">
<source>Passwort</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/user_management/login-form/login-form.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
</trans-unit>
<trans-unit id="2b62e7607770b87f7d55db9cf324fadc947c3194" datatype="html">
<source>Kombination aus Nutzername und Password ist nicht valide</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/user_management/login-form/login-form.component.html</context>
<context context-type="linenumber">24</context>
</context-group>
</trans-unit>
<trans-unit id="55de98c522f30988612c529201c4f9b94fa4bb86" datatype="html">
<source>Passwort wiederholen</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/user_management/register-form/register-form.component.html</context>
<context context-type="linenumber">24</context>
</context-group>
</trans-unit>
<trans-unit id="222db068c4ffb87ad38f26d295e23625cf38f859" datatype="html">
<source>Nutzer</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/user_management/register-form/register-form.component.html</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="ef39cbd36cd466586bdca3cffdb4866339f4aed2" datatype="html">
<source>wurde erfolgreich registriert</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/user_management/register-form/register-form.component.html</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>
......@@ -834,6 +834,166 @@
"integrity": "sha512-+CsL/HWlja9mxqyvTTqP/rpxjVeuICmTHyfAKxqpq0488N7KTRRLvWXDoL5uwc+lzqhMsaXDasReMO+ATUAUrg==",
"dev": true
},
"@angular/localize": {
"version": "10.1.4",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-10.1.4.tgz",
"integrity": "sha512-10svhkQGButoMPMqE67u4rBluucBZzeMurm8IVK1IW5G0iW+Q65w6t3Jl0L5Qga/VUEXf1ZKbbb+y09AlZGvlA==",
"dev": true,
"requires": {
"@babel/core": "7.8.3",
"glob": "7.1.2",
"yargs": "15.3.0"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
"integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
"dev": true,
"requires": {
"@babel/highlight": "^7.10.4"
}
},
"@babel/core": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.3.tgz",
"integrity": "sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@babel/generator": "^7.8.3",
"@babel/helpers": "^7.8.3",
"@babel/parser": "^7.8.3",
"@babel/template": "^7.8.3",
"@babel/traverse": "^7.8.3",
"@babel/types": "^7.8.3",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.1",
"json5": "^2.1.0",
"lodash": "^4.17.13",
"resolve": "^1.3.2",
"semver": "^5.4.1",
"source-map": "^0.5.0"
}
},
"@babel/helper-validator-identifier": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
"integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
"dev": true
},
"@babel/highlight": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
"integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@babel/types": {
"version": "7.11.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz",
"integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.10.4",
"lodash": "^4.17.19",
"to-fast-properties": "^2.0.0"
},
"dependencies": {
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true
}
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
},
"yargs": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.0.tgz",
"integrity": "sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA==",
"dev": true,
"requires": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.0"
}
}
}
},
"@angular/platform-browser": {
"version": "10.1.4",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-10.1.4.tgz",
......
......@@ -44,6 +44,7 @@
"@angular/cli": "^10.1.4",
"@angular/compiler-cli": "~10.1.4",
"@angular/language-service": "~10.1.4",
"@angular/localize": "^10.1.4",
"@graphql-codegen/add": "^1.17.7",
"@graphql-codegen/cli": "^1.17.8",
"@graphql-codegen/fragment-matcher": "^1.17.8",
......
......@@ -5,22 +5,29 @@
<div class="navbar-menu">
<div class="navbar-end">
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" i18n>Sprache</a>
<div class="navbar-dropdown">
<a *ngFor="let lang of langs" class="navbar-item" href="/{{lang}}{{getCurrentUrl()}}">{{lang}}</a>
</div>
</div>
<a class="navbar-item" target="_blank" href="https://www.uni-wuerzburg.de/sonstiges/impressum/">
Impressum
<span i18n>Impressum</span>
</a>
<a class="navbar-item" target="_blank" href="https://www.uni-wuerzburg.de/sonstiges/datenschutz/">
Datenschutz
<span i18n>Datenschutz</span>
</a>
<div class="navbar-item">
<ng-container *ngIf="currentUser; else noUserBlock">
<button (click)="logout()" class="button is-light">Logout {{currentUser.loggedInUser.username}}</button>
<button (click)="logout()" class="button is-light">
<span i18n>Logout</span>&nbsp;{{currentUser.loggedInUser.username}}</button>
</ng-container>
<ng-template #noUserBlock>
<div class="buttons">
<a routerLink="/loginForm" class="button is-light">Login</a>
<a routerLink="/registerForm" class="button is-light">Registrieren</a>
<a routerLink="/loginForm" class="button is-light" i18n>Login</a>
<a routerLink="/registerForm" class="button is-light" i18n>Registrieren</a>
</div>
</ng-template>
</div>
......
import {Component} from '@angular/core';
import {AuthenticationService} from './_services/authentication.service';
import {LoggedInUserWithTokenFragment} from './_services/apollo_services';
import {Router} from "@angular/router";
@Component({
selector: 'it4all-root',
......@@ -8,9 +9,11 @@ import {LoggedInUserWithTokenFragment} from './_services/apollo_services';
})
export class AppComponent {
readonly langs: string[] = ["de", "en"];
currentUser: LoggedInUserWithTokenFragment;
constructor(private authenticationService: AuthenticationService) {
constructor(private router: Router, private authenticationService: AuthenticationService) {
this.authenticationService.currentUser.subscribe((u) => this.currentUser = u);
}
......@@ -18,4 +21,8 @@ export class AppComponent {
this.authenticationService.logout();
}
getCurrentUrl(): string {
return this.router.url;
}
}
<div class="container">
<h1 class="title is-3 has-text-centered">Login</h1>
<h1 class="title is-3 has-text-centered" i18n>Login</h1>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div class="field">
<label for="username" class="label">Nutzername</label>
<label for="username" class="label"><span i18n>Nutzername</span>:</label>
<div class="control">
<input type="text" id="username" formControlName="username" class="input" placeholder="Nutzername" autofocus>
<input type="text" id="username" formControlName="username" class="input"
placeholder="Nutzername" i18n-placeholder autofocus>
</div>
</div>
<div class="field">
<label for="password" class="label">Passwort</label>
<label for="password" class="label"><span i18n>Passwort</span>:</label>
<div class="control">
<input type="password" formControlName="password" placeholder="Passwort" id="password" class="input">
<input type="password" formControlName="password" placeholder="Passwort" i18n-placeholder id="password"
class="input">
</div>
</div>
<div class="notification is-danger" *ngIf="submitted && inValid">
Kombination aus Nutzername und Password ist nicht valide!
<span i18n>Kombination aus Nutzername und Password ist nicht valide</span>!
</div>
<br>
<div class="field">
<div class="control">
<button class="button is-link">Login</button>
<button class="button is-link" i18n>Login</button>
</div>
</div>
......