Add translation module and lang switcher

- uses ngx-translate
- extracts all strings marked with " XXX | translate " or <X translate>
  using ngx-translate-extract (npm run extract)
- custom translation loader prevents empty strings
- default language is english
- will try to use the browsers language, will fallback to english
- functional language switching menu
- not compatible with current PO files
- current JSON-translation can be re-used
This commit is contained in:
Sean Engelhardt 2018-06-29 17:24:44 +02:00 committed by FinnStutzenstein
parent 0b6996b700
commit e605649a9b
13 changed files with 539 additions and 88 deletions

329
client/package-lock.json generated
View File

@ -391,6 +391,228 @@
"tslib": "^1.9.0"
}
},
"@biesbjerg/ngx-translate-extract": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@biesbjerg/ngx-translate-extract/-/ngx-translate-extract-2.3.4.tgz",
"integrity": "sha512-FzOdm5Jr2TMgdzTW+c6CGIgMQMCAXCyN6JYzz+hfnYjcvPrYbyR05AhM08W70nXD3a2RnbqjImNjEEcXY9pZ/g==",
"dev": true,
"requires": {
"chalk": "2.0.1",
"cheerio": "1.0.0-rc.2",
"flat": "2.0.1",
"fs": "0.0.1-security",
"gettext-parser": "1.2.2",
"glob": "7.1.2",
"mkdirp": "0.5.1",
"path": "0.12.7",
"typescript": "2.4.1",
"yargs": "8.0.2"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"camelcase": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=",
"dev": true
},
"chalk": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.0.1.tgz",
"integrity": "sha512-Mp+FXEI+FrwY/XYV45b2YD3E8i3HwnEAoFcM0qlZzq/RZ9RwWitt2Y/c7cqRAz70U7hfekqx6qNYthuKFO6K0g==",
"dev": true,
"requires": {
"ansi-styles": "^3.1.0",
"escape-string-regexp": "^1.0.5",
"supports-color": "^4.0.0"
}
},
"cliui": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
"dev": true,
"requires": {
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wrap-ansi": "^2.0.0"
},
"dependencies": {
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
}
}
},
"has-flag": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
"integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
"dev": true
},
"load-json-file": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
"integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"parse-json": "^2.2.0",
"pify": "^2.0.0",
"strip-bom": "^3.0.0"
}
},
"os-locale": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
"integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
"dev": true,
"requires": {
"execa": "^0.7.0",
"lcid": "^1.0.0",
"mem": "^1.1.0"
}
},
"path-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
"integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=",
"dev": true,
"requires": {
"pify": "^2.0.0"
}
},
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
"integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
"dev": true,
"requires": {
"load-json-file": "^2.0.0",
"normalize-package-data": "^2.3.2",
"path-type": "^2.0.0"
}
},
"read-pkg-up": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz",
"integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=",
"dev": true,
"requires": {
"find-up": "^2.0.0",
"read-pkg": "^2.0.0"
}
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dev": true,
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
},
"dependencies": {
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
}
}
},
"strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
"dev": true
},
"supports-color": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
"integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
"dev": true,
"requires": {
"has-flag": "^2.0.0"
}
},
"typescript": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.1.tgz",
"integrity": "sha1-w8yxbdqgsjFN4DHn5v7onlujRrw=",
"dev": true
},
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
},
"y18n": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"dev": true
},
"yargs": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz",
"integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=",
"dev": true,
"requires": {
"camelcase": "^4.1.0",
"cliui": "^3.2.0",
"decamelize": "^1.1.1",
"get-caller-file": "^1.0.1",
"os-locale": "^2.0.0",
"read-pkg-up": "^2.0.0",
"require-directory": "^2.1.1",
"require-main-filename": "^1.0.1",
"set-blocking": "^2.0.0",
"string-width": "^2.0.0",
"which-module": "^2.0.0",
"y18n": "^3.2.1",
"yargs-parser": "^7.0.0"
}
},
"yargs-parser": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz",
"integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=",
"dev": true,
"requires": {
"camelcase": "^4.1.0"
}
}
}
},
"@fortawesome/angular-fontawesome": {
"version": "0.1.0-10",
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.1.0-10.tgz",
@ -431,6 +653,22 @@
"webpack-sources": "^1.1.0"
}
},
"@ngx-translate/core": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-10.0.2.tgz",
"integrity": "sha512-7nM3DrJaqKswwtJlbu2kuKNl+hE8Isr18sKsKvGGpSxQk+G0gO0reDlx2PhUNus7TJTkA1C59vU/JoN8hIvZ4g==",
"requires": {
"tslib": "^1.9.0"
}
},
"@ngx-translate/http-loader": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-3.0.1.tgz",
"integrity": "sha1-ILD5i8bCUyESnT4zAqs8xInApCo=",
"requires": {
"tslib": "^1.9.0"
}
},
"@schematics/angular": {
"version": "0.6.8",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.6.8.tgz",
@ -1917,6 +2155,54 @@
}
}
},
"cheerio": {
"version": "1.0.0-rc.2",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz",
"integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=",
"dev": true,
"requires": {
"css-select": "~1.2.0",
"dom-serializer": "~0.1.0",
"entities": "~1.1.1",
"htmlparser2": "^3.9.1",
"lodash": "^4.15.0",
"parse5": "^3.0.1"
},
"dependencies": {
"domhandler": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"dev": true,
"requires": {
"domelementtype": "1"
}
},
"htmlparser2": {
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz",
"integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=",
"dev": true,
"requires": {
"domelementtype": "^1.3.0",
"domhandler": "^2.3.0",
"domutils": "^1.5.1",
"entities": "^1.1.1",
"inherits": "^2.0.1",
"readable-stream": "^2.0.2"
}
},
"parse5": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
"integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==",
"dev": true,
"requires": {
"@types/node": "*"
}
}
}
},
"chokidar": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz",
@ -2993,6 +3279,15 @@
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"dev": true
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"dev": true,
"requires": {
"iconv-lite": "~0.4.13"
}
},
"end-of-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@ -3809,6 +4104,15 @@
"locate-path": "^2.0.0"
}
},
"flat": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/flat/-/flat-2.0.1.tgz",
"integrity": "sha1-cOKRiKdL4MPIlAnu0fqVd5B64y8=",
"dev": true,
"requires": {
"is-buffer": "~1.1.2"
}
},
"flush-write-stream": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
@ -3914,6 +4218,12 @@
"readable-stream": "^2.0.0"
}
},
"fs": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
"integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=",
"dev": true
},
"fs-access": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
@ -4619,6 +4929,15 @@
"assert-plus": "^1.0.0"
}
},
"gettext-parser": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.2.2.tgz",
"integrity": "sha1-HvDadcHnWa4wicc++k0Z5AKYdI4=",
"dev": true,
"requires": {
"encoding": "0.1.12"
}
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
@ -8192,6 +8511,16 @@
"integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
"dev": true
},
"path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
"integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
"dev": true,
"requires": {
"process": "^0.11.1",
"util": "^0.10.3"
}
},
"path-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",

View File

@ -8,6 +8,7 @@
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"extract": "ngx-translate-extract -i ./src -o ./src/assets/i18n/{en,de,fr}.json --clean --sort --format-indentation ' ' --format namespaced-json",
"format:fix": "pretty-quick --staged",
"precommit": "run-s format:fix lint"
},
@ -27,6 +28,8 @@
"@fortawesome/angular-fontawesome": "0.1.0-10",
"@fortawesome/fontawesome-svg-core": "^1.2.0",
"@fortawesome/free-solid-svg-icons": "^5.1.0",
"@ngx-translate/core": "^10.0.2",
"@ngx-translate/http-loader": "^3.0.1",
"core-js": "^2.5.4",
"rxjs": "^6.2.1",
"zone.js": "^0.8.26"
@ -36,6 +39,7 @@
"@angular/cli": "~6.0.8",
"@angular/compiler-cli": "^6.0.6",
"@angular/language-service": "^6.0.6",
"@biesbjerg/ngx-translate-extract": "^2.3.4",
"@types/jasmine": "~2.8.6",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",

View File

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { OpenslidesService } from './core/services/openslides.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'app-root',
@ -7,7 +8,16 @@ import { OpenslidesService } from './core/services/openslides.service';
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
constructor(private openSlides: OpenslidesService) {}
constructor(private openSlides: OpenslidesService, public translate: TranslateService) {
// manually add the supported languages
translate.addLangs(['en', 'de', 'fr']);
// this language will be used as a fallback when a translation isn't found in the current language
translate.setDefaultLang('en');
// get the browsers default language
const browserLang = translate.getBrowserLang();
// try to use the browser language if it is available. If not, uses english.
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
}
ngOnInit() {
this.openSlides.bootup();

View File

@ -3,7 +3,7 @@ import { BrowserModule, Title } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
import { HttpClientModule, HttpClient, HttpClientXsrfModule } from '@angular/common/http';
// MaterialUI modules
import {
@ -40,6 +40,14 @@ import { WebsocketService } from './core/services/websocket.service';
import { ProjectorContainerComponent } from './projector-container/projector-container.component';
import { AlertComponent } from './core/directives/alert/alert.component';
//translation module. TODO: Potetially a SharedModule and own files
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { PruningTranslationLoader } from './core/pruning-loader';
export function HttpLoaderFactory(http: HttpClient) {
return new PruningTranslationLoader(http);
}
//add font-awesome icons to library.
//will blow up the code.
library.add(fas);
@ -78,6 +86,13 @@ library.add(fas);
MatMenuModule,
MatSnackBarModule,
FontAwesomeModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
AppRoutingModule
],
providers: [Title, ToastService, WebsocketService],

View File

@ -0,0 +1,35 @@
import { TranslateLoader } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators/';
/**
* Translation loader that replaces empty strings with nothing.
*
* ngx-translate-extract writes empty strings into json files.
* The problem is that these empty strings don't trigger
* the MissingTranslationHandler - they are simply empty strings...
*
*/
export class PruningTranslationLoader implements TranslateLoader {
constructor(private http: HttpClient, private prefix: string = '/assets/i18n/', private suffix: string = '.json') {}
public getTranslation(lang: string): any {
return this.http.get(`${this.prefix}${lang}${this.suffix}`).pipe(map((res: Object) => this.process(res)));
}
private process(object: any) {
const newObject = {};
for (const key in object) {
if (object.hasOwnProperty(key)) {
if (typeof object[key] === 'object') {
newObject[key] = this.process(object[key]);
} else if (typeof object[key] === 'string' && object[key] === '') {
// do not copy empty strings
} else {
newObject[key] = object[key];
}
}
}
return newObject;
}
}

View File

@ -4,8 +4,7 @@
<fa-icon icon='plus'></fa-icon>
</button>
<!-- TODO translate -->
<span class='app-name'>Agenda</span>
<span class='app-name' translate>Agenda</span>
<!-- download button on the right -->
<span class='spacer'></span>

View File

@ -5,7 +5,7 @@
</button>
<!-- TODO translate -->
<span class='app-name'>Motions</span>
<span class='app-name' translate>Motions</span>
<!-- download button on the right -->
<span class='spacer'></span>

View File

@ -22,13 +22,16 @@
<!-- navigation -->
<mat-nav-list>
<a mat-list-item routerLink='/' routerLinkActive='active' (click)='isMobile ? sideNav.toggle() : null'>
<fa-icon icon='home'></fa-icon> Home
<fa-icon icon='home'></fa-icon>
<span translate>Home</span>
</a>
<a mat-list-item routerLink='/agenda' routerLinkActive='active' (click)='isMobile ? sideNav.toggle() : null'>
<fa-icon icon='calendar'></fa-icon> Agenda
<fa-icon icon='calendar'></fa-icon>
<span translate>Agenda</span>
</a>
<a mat-list-item routerLink='/motions' routerLinkActive='active' (click)='isMobile ? sideNav.toggle() : null'>
<fa-icon icon='file-alt'></fa-icon> Motions
<fa-icon icon='file-alt'></fa-icon>
<span translate>Motions</span>
</a>
</mat-nav-list>
@ -52,6 +55,16 @@
<button mat-icon-button (click)='sideNav.toggle()'>
<fa-icon icon='ellipsis-v'></fa-icon>
</button>
<button mat-icon-button [matMenuTriggerFor]="languageMenu">
<fa-icon icon='language'></fa-icon>
</button>
<!-- TODO: Could use translate.getLangs() to fetch available languages-->
<mat-menu #languageMenu="matMenu">
<button mat-menu-item (click)='selectLang("en")' translate>English</button>
<button mat-menu-item (click)='selectLang("de")' translate>German</button>
<button mat-menu-item (click)='selectLang("fr")' translate>French</button>
</mat-menu>
</mat-toolbar>
<!-- continue with <mat-toolbar> in the app-->

View File

@ -7,6 +7,8 @@ import { WebsocketService } from 'app/core/services/websocket.service';
import { Subject } from 'rxjs';
import { tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; //showcase
@Component({
selector: 'app-site',
templateUrl: './site.component.html',
@ -19,7 +21,8 @@ export class SiteComponent implements OnInit {
private authService: AuthService,
private websocketService: WebsocketService,
private router: Router,
private breakpointObserver: BreakpointObserver
private breakpointObserver: BreakpointObserver,
private translate: TranslateService
) {}
ngOnInit() {
@ -45,6 +48,20 @@ export class SiteComponent implements OnInit {
socket.next(val => {
console.log('socket.next: ', val);
});
//get a translation via code: use the translation service
this.translate.get('Motions').subscribe((res: string) => {
console.log(res);
});
}
selectLang(lang: string): void {
console.log('selected langauge: ', lang);
console.log('get Langs : ', this.translate.getLangs());
this.translate.use(lang).subscribe(res => {
console.log('language changed : ', res);
});
}
logOutButton() {

View File

@ -1,10 +1,9 @@
<mat-toolbar color='primary'>
<!-- TODO translate -->
<span class='app-name'>Home</span>
<span class='app-name' translate>Home</span>
</mat-toolbar>
<div class="app-content">
[Welcome to OpenSlide]
<div class="app-content" translate>
<span>{{'Welcome to OpenSlides' | translate}}</span>
<br/>
<p translate [translateParams]="{user: 'Tim'}">Hello user</p>
</div>

View File

@ -0,0 +1,10 @@
{
"Agenda": "Tagesordnung",
"English": "Englisch",
"French": "",
"German": "Deutsch",
"Hello user": "Hallo {{user}}",
"Home": "Startseite",
"Motions": "Anträge",
"Welcome to OpenSlides": "Willkommen bei OpenSlides"
}

View File

@ -0,0 +1,10 @@
{
"Agenda": "Agenda",
"English": "English",
"French": "",
"German": "German",
"Hello user": "Hello {{user}}",
"Home": "Home",
"Motions": "Motions",
"Welcome to OpenSlides": "Welcome to OpenSlides"
}

View File

@ -0,0 +1,10 @@
{
"Agenda": "",
"English": "",
"French": "",
"German": "",
"Hello user": "",
"Home": "",
"Motions": "",
"Welcome to OpenSlides": ""
}