From 043e9aac59b021163c2abcc0bca60b3423d47ead Mon Sep 17 00:00:00 2001 From: Victor Date: Sun, 25 May 2025 10:59:26 +0200 Subject: [PATCH 1/3] Translation transformer abstract class and fake injectable --- .../lib/translation-transformer-handler.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 projects/ngx-translate/src/lib/translation-transformer-handler.ts diff --git a/projects/ngx-translate/src/lib/translation-transformer-handler.ts b/projects/ngx-translate/src/lib/translation-transformer-handler.ts new file mode 100644 index 00000000..9b0fb290 --- /dev/null +++ b/projects/ngx-translate/src/lib/translation-transformer-handler.ts @@ -0,0 +1,36 @@ +import {Injectable} from "@angular/core"; +import {InterpolatableTranslation} from "./translate.service"; + +export interface TranslationTransformerHandlerParams { + /** + * the key present in the translation files + */ + key: string; + + /** + * interpolable translation object or string returned from the store + */ + rawTranslation: InterpolatableTranslation; +} + +export abstract class TranslationTransformerHandler { + /** + * A function that transforms the interpolable translation retrieved from the translation file. + * + * @param params key and returned interpolable translation from the store + * @returns a transformed interpolable translation + * + * It returns the interpolable translation transformed by the handler logic + */ + abstract handle(params: TranslationTransformerHandlerParams): InterpolatableTranslation; +} + +/** + * This handler is just a placeholder that does nothing, in case you don't need a translation transformer handler at all + */ +@Injectable() +export class FakeTranslationTransformerHandler implements TranslationTransformerHandler { + handle(params: TranslationTransformerHandlerParams): InterpolatableTranslation { + return params.rawTranslation; + } +} From 5cc524146e7dcb9d6abe9de910c9bd9ba5bc4d09 Mon Sep 17 00:00:00 2001 From: Victor Date: Sun, 25 May 2025 11:00:04 +0200 Subject: [PATCH 2/3] Translation transformer integration inside Translate service --- projects/ngx-translate/src/lib/translate.service.ts | 12 +++++++++++- projects/ngx-translate/src/public-api.ts | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/projects/ngx-translate/src/lib/translate.service.ts b/projects/ngx-translate/src/lib/translate.service.ts index 90d790ba..048ff711 100644 --- a/projects/ngx-translate/src/lib/translate.service.ts +++ b/projects/ngx-translate/src/lib/translate.service.ts @@ -7,6 +7,7 @@ import {TranslateLoader} from "./translate.loader"; import {InterpolateFunction, TranslateParser} from "./translate.parser"; import {TranslateStore} from "./translate.store"; import {isDefinedAndNotNull, isArray, isString, isDict, insertValue} from "./util"; +import {TranslationTransformerHandler} from "./translation-transformer-handler"; export const ISOLATE_TRANSLATE_SERVICE = new InjectionToken('ISOLATE_TRANSLATE_SERVICE'); export const USE_DEFAULT_LANG = new InjectionToken('USE_DEFAULT_LANG'); @@ -153,6 +154,7 @@ export class TranslateService { * @param compiler An instance of the compiler currently used * @param parser An instance of the parser currently used * @param missingTranslationHandler A handler for missing translations. + * @param translationTransformerHandler A handler to transform retrieved raw translation before interpolation * @param useDefaultLang whether we should use default language translation when current language translation is missing. * @param isolate whether this service should use the store or not * @param extend To make a child module extend (and use) translations from parent modules. @@ -163,6 +165,7 @@ export class TranslateService { public compiler: TranslateCompiler, public parser: TranslateParser, public missingTranslationHandler: MissingTranslationHandler, + public translationTransformerHandler: TranslationTransformerHandler, @Inject(USE_DEFAULT_LANG) private useDefaultLang = true, @Inject(ISOLATE_TRANSLATE_SERVICE) isolate = false, @Inject(USE_EXTEND) private extend = false, @@ -347,7 +350,14 @@ export class TranslateService { private getTextToInterpolate(key: string): InterpolatableTranslation | undefined { - return this.store.getTranslation(key, this.useDefaultLang); + const rawTranslation = this.store.getTranslation(key, this.useDefaultLang); + + return this.translationTransformerHandler.handle( + { + key, + rawTranslation, + } + ); } private runInterpolation(translations: InterpolatableTranslation, interpolateParams?: InterpolationParameters): Translation diff --git a/projects/ngx-translate/src/public-api.ts b/projects/ngx-translate/src/public-api.ts index 811a1f16..4ff9e000 100644 --- a/projects/ngx-translate/src/public-api.ts +++ b/projects/ngx-translate/src/public-api.ts @@ -13,6 +13,7 @@ import { ISOLATE_TRANSLATE_SERVICE, TranslateService } from "./lib/translate.service"; +import {FakeTranslationTransformerHandler, TranslationTransformerHandler} from "./lib/translation-transformer-handler"; export * from "./lib/translate.loader"; export * from "./lib/translate.service"; @@ -24,12 +25,14 @@ export * from "./lib/translate.pipe"; export * from "./lib/translate.store"; export * from "./lib/extraction-marker"; export * from "./lib/util" +export * from "./lib/translation-transformer-handler" export interface TranslateModuleConfig { loader?: Provider; compiler?: Provider; parser?: Provider; missingTranslationHandler?: Provider; + translationTransformerHandler?: Provider; // isolate the service instance, only works for lazy loaded modules or components with the "providers" property isolate?: boolean; // extends translations for a given language instead of ignoring them if present @@ -45,6 +48,7 @@ export const provideTranslateService = (config: TranslateModuleConfig = {}): Env config.compiler || {provide: TranslateCompiler, useClass: TranslateFakeCompiler}, config.parser || {provide: TranslateParser, useClass: TranslateDefaultParser}, config.missingTranslationHandler || {provide: MissingTranslationHandler, useClass: FakeMissingTranslationHandler}, + config.translationTransformerHandler || {provide: TranslationTransformerHandler, useClass: FakeTranslationTransformerHandler}, TranslateStore, {provide: ISOLATE_TRANSLATE_SERVICE, useValue: config.isolate}, {provide: USE_DEFAULT_LANG, useValue: config.useDefaultLang}, @@ -77,6 +81,7 @@ export class TranslateModule { config.compiler || {provide: TranslateCompiler, useClass: TranslateFakeCompiler}, config.parser || {provide: TranslateParser, useClass: TranslateDefaultParser}, config.missingTranslationHandler || {provide: MissingTranslationHandler, useClass: FakeMissingTranslationHandler}, + config.translationTransformerHandler || {provide: TranslationTransformerHandler, useClass: FakeTranslationTransformerHandler}, TranslateStore, {provide: ISOLATE_TRANSLATE_SERVICE, useValue: config.isolate}, {provide: USE_DEFAULT_LANG, useValue: config.useDefaultLang}, @@ -98,6 +103,7 @@ export class TranslateModule { config.compiler || {provide: TranslateCompiler, useClass: TranslateFakeCompiler}, config.parser || {provide: TranslateParser, useClass: TranslateDefaultParser}, config.missingTranslationHandler || {provide: MissingTranslationHandler, useClass: FakeMissingTranslationHandler}, + config.translationTransformerHandler || {provide: TranslationTransformerHandler, useClass: FakeTranslationTransformerHandler}, {provide: ISOLATE_TRANSLATE_SERVICE, useValue: config.isolate}, {provide: USE_DEFAULT_LANG, useValue: config.useDefaultLang}, {provide: USE_EXTEND, useValue: config.extend}, From 1dbff34f5341ad93fc6dc09bdaf8bdcb015d9718 Mon Sep 17 00:00:00 2001 From: Victor Date: Sun, 25 May 2025 11:00:31 +0200 Subject: [PATCH 3/3] Translation transformer tests --- .../translation-transformer-handler.spec.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 projects/ngx-translate/src/lib/translation-transformer-handler.spec.ts diff --git a/projects/ngx-translate/src/lib/translation-transformer-handler.spec.ts b/projects/ngx-translate/src/lib/translation-transformer-handler.spec.ts new file mode 100644 index 00000000..862aecd5 --- /dev/null +++ b/projects/ngx-translate/src/lib/translation-transformer-handler.spec.ts @@ -0,0 +1,119 @@ +import {TranslationObject} from "./translate.service"; +import {isDict, provideTranslateService, TranslateService, TranslationTransformerHandler} from "../public-api"; +import {Injectable} from "@angular/core"; +import {TranslationTransformerHandlerParams} from "./translation-transformer-handler"; +import {TestBed} from "@angular/core/testing"; +import {TranslateLoader} from "./translate.loader"; +import {Observable, of, tap} from "rxjs"; + +const translations: TranslationObject = { + "TEST": "This is a test", + "dictionary": { + "_": "default translation", + "first": "First translation {{placeholder}}", + } +}; + +@Injectable() +class FakeLoader implements TranslateLoader { + getTranslation(lang: string): Observable { + void lang; + return of(translations); + } +} + +describe('TranslationTransformerHandler', () => { + let translate: TranslateService; + let translationHandler: TranslationTransformerHandler; + + @Injectable() + class CustomTranslationHandlerService implements TranslationTransformerHandler { + handle(params: TranslationTransformerHandlerParams): unknown { + if (isDict(params.rawTranslation)) { + return params.key; + } + + return params.rawTranslation; + } + } + + const prepareCustomHandler = () => { + TestBed.configureTestingModule({ + providers: [ + provideTranslateService({ + loader: {provide: TranslateLoader, useClass: FakeLoader}, + translationTransformerHandler: {provide: TranslationTransformerHandler, useClass: CustomTranslationHandlerService}, + }) + ] + }); + + translate = TestBed.inject(TranslateService); + translationHandler = TestBed.inject(TranslationTransformerHandler); + translate.use('en'); + }; + + const prepareDefaultHandler = () => { + TestBed.configureTestingModule({ + providers: [ + provideTranslateService({ + loader: {provide: TranslateLoader, useClass: FakeLoader}, + }) + ] + }); + + translate = TestBed.inject(TranslateService); + translationHandler = TestBed.inject(TranslationTransformerHandler); + translate.use('en'); + }; + + it('should use the standard FakeTranslationHandler and return an interpolated dictionary', () => { + prepareDefaultHandler(); + translate.get("dictionary", {placeholder: "testPlaceholder"}) + .pipe( + tap((result) => { + expect(result).toEqual({ + "_": "default translation", + "first": "First translation testPlaceholder", + }); + }) + ) + .subscribe(); + }) + + it('should use the standard FakeTranslationHandler and return the translation string', () => { + prepareDefaultHandler(); + translate.get("TEST") + .pipe( + tap((result) => { + expect(result).toEqual(translations["TEST"]); + }) + ) + .subscribe(); + }) + + it('should use CustomTranslationHandler and return the translation string', () => { + prepareCustomHandler(); + spyOn(translationHandler, 'handle').and.callThrough(); + translate.get("TEST") + .pipe( + tap((result) => { + expect(translationHandler.handle).toHaveBeenCalledWith(jasmine.objectContaining({key: "TEST", rawTranslation: translations["TEST"]})); + expect(result).toEqual(translations["TEST"]); + }) + ) + .subscribe(); + }) + + it('should use CustomTranslationHandler and return the key of the dictionary', () => { + prepareCustomHandler(); + spyOn(translationHandler, 'handle').and.callThrough(); + translate.get("dictionary") + .pipe( + tap((result) => { + expect(translationHandler.handle).toHaveBeenCalledWith(jasmine.objectContaining({key: "dictionary", rawTranslation: translations["dictionary"]})); + expect(result).toEqual("dictionary"); + }) + ) + .subscribe(); + }) +})