From d20f8d6f448e310d57543e94ba8a862495544157 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Thu, 8 Nov 2018 15:34:43 +0100 Subject: [PATCH] Use own translation service wrapper for using custom translations --- client/src/app/app.module.ts | 21 +---- .../translate/openslides-translate-module.ts | 61 ++++++++++++++ .../app/core/translate/translation-parser.ts | 67 +++++++++++++++ .../translation-pruning-loader.ts} | 18 ++-- .../app/core/translate/translation-service.ts | 82 +++++++++++++++++++ client/src/app/shared/shared.module.ts | 4 +- .../config-field/config-field.component.ts | 4 + client/src/e2e-imports.module.ts | 16 ++-- 8 files changed, 236 insertions(+), 37 deletions(-) create mode 100644 client/src/app/core/translate/openslides-translate-module.ts create mode 100644 client/src/app/core/translate/translation-parser.ts rename client/src/app/core/{pruning-loader.ts => translate/translation-pruning-loader.ts} (85%) create mode 100644 client/src/app/core/translate/translation-service.ts diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 8c0a9f0f5..2abf1dc80 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -2,7 +2,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgModule, APP_INITIALIZER } from '@angular/core'; -import { HttpClientModule, HttpClient, HttpClientXsrfModule } from '@angular/common/http'; +import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http'; import { PapaParseModule } from 'ngx-papaparse'; // Elementary App Components @@ -11,25 +11,16 @@ import { AppComponent } from './app.component'; import { CoreModule } from './core/core.module'; // translation module. -import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; -import { PruningTranslationLoader } from './core/pruning-loader'; import { LoginModule } from './site/login/login.module'; import { AppLoadService } from './core/services/app-load.service'; import { ProjectorModule } from './site/projector/projector.module'; import { SlidesModule } from './slides/slides.module'; +import { OpenSlidesTranslateModule } from './core/translate/openslides-translate-module'; // PWA import { ServiceWorkerModule } from '@angular/service-worker'; import { environment } from '../environments/environment'; -/** - * For the translation module. Loads a Custom 'translation loader' and provides it as loader. - * @param http Just the HttpClient to load stuff - */ -export function HttpLoaderFactory(http: HttpClient): PruningTranslationLoader { - return new PruningTranslationLoader(http); -} - /** * Returns a function that returns a promis that will be resolved, if all apps are loaded. * @param appLoadService The service that loads the apps. @@ -51,13 +42,7 @@ export function AppLoaderFactory(appLoadService: AppLoadService): () => Promise< headerName: 'X-CSRFToken' }), BrowserAnimationsModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient] - } - }), + OpenSlidesTranslateModule.forRoot(), AppRoutingModule, CoreModule, LoginModule, diff --git a/client/src/app/core/translate/openslides-translate-module.ts b/client/src/app/core/translate/openslides-translate-module.ts new file mode 100644 index 000000000..607dc5b15 --- /dev/null +++ b/client/src/app/core/translate/openslides-translate-module.ts @@ -0,0 +1,61 @@ +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { + TranslateModule, + TranslateLoader, + TranslateFakeCompiler, + TranslateParser, + TranslateCompiler, + FakeMissingTranslationHandler, + MissingTranslationHandler, + TranslateStore, + USE_STORE, + USE_DEFAULT_LANG, + TranslateService, + TranslatePipe, + TranslateDirective +} from '@ngx-translate/core'; +import { HttpClient } from '@angular/common/http'; +import { OpenSlidesTranslateParser } from './translation-parser'; +import { PruningTranslationLoader } from './translation-pruning-loader'; +import { OpenSlidesTranslateService } from './translation-service'; + +/** + * This is analog to the TranslateModule from ngx-translate, but with our own classes. + */ +@NgModule({ + imports: [TranslateModule], + exports: [TranslatePipe, TranslateDirective] +}) +export class OpenSlidesTranslateModule { + public static forRoot(): ModuleWithProviders { + return { + ngModule: TranslateModule, + providers: [ + { provide: TranslateLoader, useClass: PruningTranslationLoader, deps: [HttpClient] }, + { provide: TranslateCompiler, useClass: TranslateFakeCompiler }, + { provide: TranslateParser, useClass: OpenSlidesTranslateParser }, + { provide: MissingTranslationHandler, useClass: FakeMissingTranslationHandler }, + TranslateStore, + { provide: USE_STORE, useValue: false }, + { provide: USE_DEFAULT_LANG, useValue: true }, + { provide: TranslateService, useClass: OpenSlidesTranslateService } + ] + }; + } + + // no config store for child. + public static forChild(): ModuleWithProviders { + return { + ngModule: TranslateModule, + providers: [ + { provide: TranslateLoader, useClass: PruningTranslationLoader, deps: [HttpClient] }, + { provide: TranslateCompiler, useClass: TranslateFakeCompiler }, + { provide: TranslateParser, useClass: OpenSlidesTranslateParser }, + { provide: MissingTranslationHandler, useClass: FakeMissingTranslationHandler }, + { provide: USE_STORE, useValue: false }, + { provide: USE_DEFAULT_LANG, useValue: true }, + { provide: TranslateService, useClass: OpenSlidesTranslateService } + ] + }; + } +} diff --git a/client/src/app/core/translate/translation-parser.ts b/client/src/app/core/translate/translation-parser.ts new file mode 100644 index 000000000..9beeadaab --- /dev/null +++ b/client/src/app/core/translate/translation-parser.ts @@ -0,0 +1,67 @@ +import { TranslateDefaultParser, TranslateStore } from '@ngx-translate/core'; +import { ConfigService } from '../services/config.service'; +import { Injectable } from '@angular/core'; + +interface CustomTranslation { + original: string; + translation: string; +} + +type CustomTranslations = CustomTranslation[]; + +/** + * Custom translate parser. Intercepts and use custom translations from the configservice. + */ +@Injectable() +export class OpenSlidesTranslateParser extends TranslateDefaultParser { + /** + * Saves the custom translations retrieved from the config service + */ + private customTranslations: CustomTranslations = []; + + /** + * Subscribes to the config services and watches for updated custom translations. + * + * @param config + * @param translateStore + */ + public constructor(config: ConfigService, private translateStore: TranslateStore) { + super(); + + config.get('translations').subscribe(ct => { + if (!ct) { + ct = []; + } + this.customTranslations = ct; + + // trigger reload of all languages. This does not hurt performance, + // in fact the directives and pipes just listen to the selected language. + this.translateStore.langs.forEach(lang => { + this.translateStore.onTranslationChange.emit({ + lang: lang, + translations: this.translateStore.translations[lang] + }); + }); + }); + } + + /** + * Here, we actually intercept getting translations. This method is called from the + * TranslateService trying to retrieve a translation to the key. + * + * Here, the translation is searched and then overwritten by our custom translations, if + * the value exist. + * + * @param target The translation dict + * @param key The key to find the translation + */ + public getValue(target: any, key: string): any { + const translation = super.getValue(target, key); + const customTranslation = this.customTranslations.find(c => c.original === translation); + if (customTranslation) { + return customTranslation.translation; + } else { + return translation; + } + } +} diff --git a/client/src/app/core/pruning-loader.ts b/client/src/app/core/translate/translation-pruning-loader.ts similarity index 85% rename from client/src/app/core/pruning-loader.ts rename to client/src/app/core/translate/translation-pruning-loader.ts index 6c6ab4e61..64b34c6d9 100644 --- a/client/src/app/core/pruning-loader.ts +++ b/client/src/app/core/translate/translation-pruning-loader.ts @@ -12,18 +12,22 @@ import { Observable } from 'rxjs'; * */ export class PruningTranslationLoader implements TranslateLoader { + /** + * Path to the language files. Can be adjusted of needed + */ + private prefix = '/assets/i18n/'; + + /** + * Suffix of the translation files. Usually '.json'. + */ + private suffix = '.json'; + /** * Constructor to load the HttpClient * * @param http httpClient to load the translation files. - * @param prefix Path to the language files. Can be adjusted of needed - * @param suffix Suffix of the translation files. Usually '.json'. */ - public constructor( - private http: HttpClient, - private prefix: string = '/assets/i18n/', - private suffix: string = '.json' - ) {} + public constructor(private http: HttpClient) {} /** * Loads a language file, stores the content, give it to the process function. diff --git a/client/src/app/core/translate/translation-service.ts b/client/src/app/core/translate/translation-service.ts new file mode 100644 index 000000000..e9b35e7bf --- /dev/null +++ b/client/src/app/core/translate/translation-service.ts @@ -0,0 +1,82 @@ +import { + TranslateStore, + TranslateService, + TranslateLoader, + TranslateCompiler, + TranslateParser, + MissingTranslationHandler, + USE_DEFAULT_LANG, + USE_STORE +} from '@ngx-translate/core'; +import { Inject, Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; + +/** + * Custom translate service. Wraps the get, stream and instant method not to throw an error, if null or undefined + * is passed as keys to them. This happens, if yet not resolved properties should be translated in the templates. + * Returns empty strings instead. + */ +@Injectable() +export class OpenSlidesTranslateService extends TranslateService { + /** + * See the ngx-translate TranslateService for docs. + * + * @param store + * @param currentLoader + * @param compiler + * @param parser + * @param missingTranslationHandler + * @param useDefaultLang + * @param isolate + */ + public constructor( + store: TranslateStore, + currentLoader: TranslateLoader, + compiler: TranslateCompiler, + parser: TranslateParser, + missingTranslationHandler: MissingTranslationHandler, + @Inject(USE_DEFAULT_LANG) useDefaultLang: boolean = true, + @Inject(USE_STORE) isolate: boolean = false + ) { + super(store, currentLoader, compiler, parser, missingTranslationHandler, useDefaultLang, isolate); + } + + /** + * Uses the original get function and returns an empty string instead of throwing an error. + * + * @override + */ + public get(key: string | Array, interpolateParams?: Object): Observable { + try { + return super.get(key, interpolateParams); + } catch { + return of(''); + } + } + + /** + * Uses the original key function and returns an empty string instead of throwing an error. + * + * @override + */ + public stream(key: string | Array, interpolateParams?: Object): Observable { + try { + return super.stream(key, interpolateParams); + } catch { + return of(''); + } + } + + /** + * Uses the original instant function and returns an empty string instead of throwing an error. + * + * @override + */ + public instant(key: string | Array, interpolateParams?: Object): string | any { + try { + return super.instant(key, interpolateParams); + } catch { + return ''; + } + } +} diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 95d7c0241..238dc8276 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -78,6 +78,7 @@ import { ProjectorButtonComponent } from './components/projector-button/projecto import { ProjectionDialogComponent } from './components/projection-dialog/projection-dialog.component'; import { ResizedDirective } from './directives/resized.directive'; import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-block.component'; +import { OpenSlidesTranslateModule } from '../core/translate/openslides-translate-module'; /** * Share Module for all "dumb" components and pipes. @@ -128,7 +129,7 @@ import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-b MatTabsModule, MatSliderModule, DragDropModule, - TranslateModule.forChild(), + OpenSlidesTranslateModule.forChild(), RouterModule, NgxMatSelectSearchModule, FileDropModule, @@ -171,6 +172,7 @@ import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-b NgxMatSelectSearchModule, FileDropModule, TranslateModule, + OpenSlidesTranslateModule, PermsDirective, DomChangeDirective, AutofocusDirective, diff --git a/client/src/app/site/config/components/config-field/config-field.component.ts b/client/src/app/site/config/components/config-field/config-field.component.ts index f76af3824..e04d9e63f 100644 --- a/client/src/app/site/config/components/config-field/config-field.component.ts +++ b/client/src/app/site/config/components/config-field/config-field.component.ts @@ -155,6 +155,10 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit { if (this.configItem.inputType === 'datetimepicker') { value = Date.parse(value); } + // TODO: Solve this via a custom input form. + if (this.configItem.inputType === 'translations') { + value = JSON.parse(value); + } this.debounceTimeout = null; this.repo.update({ value: value }, this.configItem).then(() => { this.error = null; diff --git a/client/src/e2e-imports.module.ts b/client/src/e2e-imports.module.ts index f24c12518..8e55077ed 100644 --- a/client/src/e2e-imports.module.ts +++ b/client/src/e2e-imports.module.ts @@ -1,12 +1,12 @@ import { NgModule } from '@angular/core'; import { APP_BASE_HREF, CommonModule } from '@angular/common'; -import { HttpClient, HttpClientModule } from '@angular/common/http'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { HttpClientModule } from '@angular/common/http'; import { SharedModule } from 'app/shared/shared.module'; -import { AppModule, HttpLoaderFactory } from 'app/app.module'; +import { AppModule } from 'app/app.module'; import { AppRoutingModule } from 'app/app-routing.module'; import { LoginModule } from 'app/site/login/login.module'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { OpenSlidesTranslateModule } from 'app/core/translate/openslides-translate-module'; /** * Share Module for all "dumb" components and pipes. @@ -24,18 +24,12 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; CommonModule, SharedModule, HttpClientModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient] - } - }), + OpenSlidesTranslateModule.forRoot(), LoginModule, BrowserAnimationsModule, AppRoutingModule ], - exports: [CommonModule, SharedModule, HttpClientModule, TranslateModule, AppRoutingModule], + exports: [CommonModule, SharedModule, HttpClientModule, OpenSlidesTranslateModule, AppRoutingModule], providers: [{ provide: APP_BASE_HREF, useValue: '/' }] }) export class E2EImportsModule {}