diff --git a/client/package-lock.json b/client/package-lock.json index 8e0501f64..e38140ad1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/package.json b/client/package.json index c557b70a9..60e374a6a 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index a94cae2bd..4a716d26e 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -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(); diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 79b7fadb5..96ecf55a5 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -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], diff --git a/client/src/app/core/pruning-loader.ts b/client/src/app/core/pruning-loader.ts new file mode 100644 index 000000000..4e264fd04 --- /dev/null +++ b/client/src/app/core/pruning-loader.ts @@ -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; + } +} diff --git a/client/src/app/site/agenda/agenda.component.html b/client/src/app/site/agenda/agenda.component.html index 4a02263ad..e061ae779 100644 --- a/client/src/app/site/agenda/agenda.component.html +++ b/client/src/app/site/agenda/agenda.component.html @@ -1,20 +1,19 @@ - + - - Agenda + Agenda - - - + + +
- Agenda Works + Agenda Works
\ No newline at end of file diff --git a/client/src/app/site/motions/motions.component.html b/client/src/app/site/motions/motions.component.html index 49e716735..c1de00957 100644 --- a/client/src/app/site/motions/motions.component.html +++ b/client/src/app/site/motions/motions.component.html @@ -1,20 +1,20 @@ - + - - Motions + + Motions - - - + + +
- Motion Works + Motion Works
\ No newline at end of file diff --git a/client/src/app/site/site.component.html b/client/src/app/site/site.component.html index 7b85a1694..39d0a480e 100644 --- a/client/src/app/site/site.component.html +++ b/client/src/app/site/site.component.html @@ -1,64 +1,77 @@ - - + + - - - OpenSlides-logo - + + + OpenSlides-logo + + + + + + + + UserName + + + + + + + + + + + Home + + + + Agenda + + + + Motions + + + + + + + + + + + + + + + + + + + + + + + + - - - - - UserName - - - - - + + - - - - Home - - - Agenda - - - Motions - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/client/src/app/site/site.component.ts b/client/src/app/site/site.component.ts index 466235a61..73dc48f0c 100644 --- a/client/src/app/site/site.component.ts +++ b/client/src/app/site/site.component.ts @@ -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() { diff --git a/client/src/app/site/start/start.component.html b/client/src/app/site/start/start.component.html index 63c2c1839..3565b4bd5 100644 --- a/client/src/app/site/start/start.component.html +++ b/client/src/app/site/start/start.component.html @@ -1,10 +1,9 @@ - - - Home - + Home -
- [Welcome to OpenSlide] +
+ {{'Welcome to OpenSlides' | translate}} +
+

Hello user

\ No newline at end of file diff --git a/client/src/assets/i18n/de.json b/client/src/assets/i18n/de.json new file mode 100644 index 000000000..8f0dffb58 --- /dev/null +++ b/client/src/assets/i18n/de.json @@ -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" +} diff --git a/client/src/assets/i18n/en.json b/client/src/assets/i18n/en.json new file mode 100644 index 000000000..6a2320989 --- /dev/null +++ b/client/src/assets/i18n/en.json @@ -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" +} diff --git a/client/src/assets/i18n/fr.json b/client/src/assets/i18n/fr.json new file mode 100644 index 000000000..ae7963a9d --- /dev/null +++ b/client/src/assets/i18n/fr.json @@ -0,0 +1,10 @@ +{ + "Agenda": "", + "English": "", + "French": "", + "German": "", + "Hello user": "", + "Home": "", + "Motions": "", + "Welcome to OpenSlides": "" +}