Use own translation service wrapper for using custom translations

This commit is contained in:
FinnStutzenstein 2018-11-08 15:34:43 +01:00
parent d705b2a137
commit d20f8d6f44
8 changed files with 236 additions and 37 deletions

View File

@ -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,

View File

@ -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 }
]
};
}
}

View File

@ -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<CustomTranslations>('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;
}
}
}

View File

@ -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.

View File

@ -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<string>, interpolateParams?: Object): Observable<string | any> {
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<string>, interpolateParams?: Object): Observable<string | any> {
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<string>, interpolateParams?: Object): string | any {
try {
return super.instant(key, interpolateParams);
} catch {
return '';
}
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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 {}