📜 ⬆️ ⬇️

Angular: ngx-translate lifehacks. TranslateCompiler

Good day.


In the near future I plan to publish some ngx-translate life hacks.



 <p>This entity created at 15:47 01-02-2003 by Some Guy</p> 

Add a new field to en.json for this line:


 "ENTITY_CREATED_AT": "This entity created at {{createdAtDate}} by {{guyName}}" 

In the view of our component, naturally, we add a translate directive or pipe.


 <div translate="ENTITY_CREATED_AT" [translateParams]="{createdAtDate: entity.createdAt, guyName: entity.name}"></div> 

But with this approach, a problem arises - the createdAtDate field should already be localized, otherwise we will see just the result of new Date().toString().


We have 2a solutions to this problem:


Some best practice!

I prefer to transfer to translateParams an independent object the place to tie up the fields in jsonʻe to the fields of the real model.
I mean:


 //our Entity model class Entity { createdAt: Date; name: string; someComplexField: ComplexType; } //inside some HTML [translateParams]="{createdAtDate: entity.createdAt, guyName: entity.name}" // separate object 

of course, it was possible in jsonʻe to refer to the fields of the real model, clearly indicating the field names in the Entity model:


 "ENTITY_CREATED_AT": "This entity created at {{createdAt}} by {{name}}" //  entity.createdAt, entity.name 

and in translateParams just pass the entity:


 <div translate="ENTITY_CREATED_AT" [translateParams]="entity"></div> 

And what if we already have support for 10 languages ​​and suddenly the model changes the name of one of the fields, which is tied to ngx-translate?


Help function inside the view component.

We can create a function inside the view component that will return to us a ready object for transfer to translateParams


 public getEntityTranslateParam(dateFormat: string) { return { createdAtDate: moment.format(dateFormat, entity.createdAt), //      guyName: entity.name }; } 

There is a drawback - we have to clutter up each component with such functions. Of course, we can add some service that will create such objects for us, so that the code of our components is a bit cleaner, this only makes sense if the objects are large enough, because we still need to inject this service everywhere.


Split ENTITY_CREATED_AT into two parts.

 "ENTITY_CREATED_AT_1": "This entity created at", "ENTITY_CREATED_AT_2": "by {{guyName}}" 

Here it grows and becomes difficult to read HTML, but now we can use any kind of pipe! (Of course, you can use the place-pipe directive, but you will have to create another html inside the div)


 <div> {{'ENTITY_CREATED_AT_1' | translate}} {{entity.createdAt | dateFormatPipe:LL}} {{'ENTITY_CREATED_AT_2' | translate:{guyName:entity.guyName} }} </div> 

Praise the ngx-translate developer for a great infrastructure!

When importing the ngx-translate module into our application, we can provide a custom Translateompiler .
By the way, here is a link to a great plural \ gender etc. compiler that works with ngx-translate.


Our task is to write our own TranslateCompiler, which will be able to execute pipes within ngx-translate localizations.
Let's start with the preparation of DI (because we will take the pipes from Injector`a), and the initialization of ngx-translate.


Add the necessary pipe to the providers of the module, in my case it is a SharedModule because the application contains more than one module.


 @NgModule({ //       imports: [ ...exportedModules ], declarations: [ ...exportedDeclarations ], exports: [ ...exportedModules, ...exportedDeclarations ], providers: [ // declare pipes available with injector [ DateFormatPipe, {provide: 'dateFormat', useExisting: DateFormatPipe} ] ] }) export class SharedModule { } 

Now we initialize ngx-translate and give it our CustomTranslateCompiler .
In my case, I use CoreModule .


 import { CustomTranslateCompiler } from 'app/_core/services/translate/translate.compiler'; @NgModule({ // some needed providers imports: [ CommonModule, TranslateModule.forRoot({ compiler: { provide: TranslateCompiler, useClass: CustomTranslateCompiler, deps: [Injector] }, }) ], exports: [CommonModule, TranslateModule] }) export class CoreModule { } 

We can use only those pipelines that were announced in the providers module (in the example, dateFormst pipe)


 "ENTITY_CREATED_AT": "This entity created at {{createdAtDate | dateFormat:LL}} by {{guyName}}" 

Determine, by default, PipeTranslateCompiler ;


 export class PipeTranslateCompiler implements TranslateCompiler { constructor(private injector: Injector, private errorHandler: ErrorHandler) { } public compile(value: string, lang: string): string | Function { return value; } public compileTranslations(translations: any, lang: string): any { return translations; } } 

We have 2e functions:
compileTranslations - our loaded json (which you could download via TranslateHttpLoader) gets at the input
compile - gets only one value, and will be called when we explicitly add some fields to the ngx-translate via TranslateService.set('some translate val', 'key', 'en') .


Since a large enough json can get into compileTranslations, we will need to recursively walk through the fields of the entire object and parse each value, I would like to know in advance which fields to parse and which not. Therefore, I decided to save all fields that need to be saved via PipeTranslateCompiler with the @ symbol.


 "@ENTITY_CREATED_AT": "This entity created at {{createdAtDate | dateFormat:LL}} by {{guyName}}", //     "OTHER_KEY": "This is regular entity" //     

And now let's pay attention to the return type of the function f-tsii compile - string | Function compile - string | Function , this means that we can turn any string value into a function, and when we in our HTML view use the translate pipe or directive, ngx-translate, knowing that if there is a function with a key, it will call it with parameters, which we pass to translateParams.


 <div>{{'ENTITY_CREATED_AT' | translate:paramsObject}}</div> //  ENTITY_CREATED_AT -  transform      paramsObject. 

So we need to parse the string "This entity created at {{createdAtDate | dateFormat:LL}} by {{guyName}}" and replace it with the function.


First, let's imagine how we will store the result of parsing the string, so that later on the parameter base you can quickly get a string with the pipes already applied.
I want to be able to parse 2+ pipes at once with 2+ parameters


 "SOME_KEY": "value1: {{dateField | dateFormat:LL}} and value2: {{anotherField | customPipe:param1:param2}} 

So we need to parse the string with a regular expression, selecting the parts inside the brackets {{. *}}.
For each highlighted bracket, you need information about what's inside, tobish the name of the pipe, an array of parameters of the pipe and the name of the field of the object.


 {{__ | _:1:2}} 

Define a couple of interfaces for convenience:


 interface PipedObject { property: string; // __ pipe: PipeDefinition; //  } interface PipeDefinition { name: string; // _ params: string[]; //   } 

Parsing function:


 private parseTranslation(res: string): {pipedObjects: PipedObject[], matches: string[]} { //   "{{dateField | dateFormat:LL | additionalPipe:param1}}", "{{anotherField | customPipe:param1:param2}}" let matches = res.match(/{{.[^{{]*}}/g); let pipedObjects: PipedObject[] = []; (matches || []).forEach((v) => { //     {{dateField | dateFormat:LL}} -> dateField|dateFormat:LL|additionalPipe:param1 v = v.replace(/[{}\s]+/g, ''); //  : dateField|dateFormat:LL|additionalPipe:param1 let pipes = v.split('|'); let objectPropertyName = pipes[0]; // dateField pipes = pipes.slice(1); // [dateFormat:LL, additionalPipe:param1] for(let pipe of pipes) { // customPipe:param1:param2 -> ['customPipe', 'param1', 'param2'] let pipeTokens = pipe.split(':'); pipedObjects.push({ property: objectPropertyName, pipe: { name: pipeTokens[0], // customPipe params: pipeTokens.slice(1) // ['param1', 'param2'] } }); } }); return {pipedObjects, matches}; } 

Now, for a start, let's connect the parseTranslation and the function that compiles only one value - compile


 public compile(value: string, lang: string): string | Function { return this.compileValue(value); } private compileValue(val): Function { let parsedTranslation = this.parseTranslation(val); // ,    return (argsObj: object) => { //  -     ngx-translate        //      : value1: {{dateField | dateFormat:LL}} and value2: {{anotherField | customPipe:param1:param2}} let res = val; parsedTranslation.pipedObjects.forEach((o, i) => { //   DI   ,        . const pipe = this.injector.get(o.pipe.name); const property = argsObj[o.property]; // argsObj -  -   [translateParams] //           pipe.transform(prop, o.pipe.params); const pipeParams = this.assignPipeParams(argsObj, o.pipe.params || []); if(!property) { return res; } let pipedValue = pipe.transform( property, pipeParams.length === 1 ? pipeParams[0] : pipeParams ); //  {{*.}}    . {{dateField | dateFormat:LL}} -> 15:00 res = res.replace(parsedTranslation.matches[i], pipedValue); }); return res; }; } private assignPipeParams(obj: object, params: string[]) { let assignedParams = []; params.forEach(p => { if(obj.hasOwnProperty(p)) { assignedParams.push(obj[p]); } else { assignedParams.push(p); } }); return assignedParams; } 

For clarity, assignPipeParams allows us to pass parameters to a customPipe from a component.


 "@SOME": "value1: {{dateField | dateFormat:LL}} and value2: {{anotherField | customPipe:param1}} //      :LL  :param1 -        <div>{{'@SOME' | myTranslate:{dateField: entity.value1, anotherField: entity.value2, param1: 'DYNAMIC_VALUE'} }}</div> //    :param1   

I will not describe the method of parsing the entire json'a from compileTranslations, on the stack overflow you can find many methods of circumventing the entire json'a structure.


 public compileTranslations(translations: any, lang: string): any { this.iterateTranslations(translations); return translations; } private iterateTranslations(obj) { for (let key in obj) { if (obj.hasOwnProperty(key)) { const val = obj[key]; const isString = typeof val === 'string'; if(key[0] === '@' && isString) { obj[key] = this.compileValue(val); //   compileValue } else if(!isString) { this.iterateTranslations(val); } } } } 

Done! Now we can use the pipes inside the ngx-translate json `s!


New useful Angular 6 feature

Here you can see briefly and in fact what Angular Elements is.
There is a source from the video


In general, because Angular Elements is still experimental, we still cannot compile web components from Angular and use them in any JavaScript application.


But! nevertheless, the benefits are already there. Now we can create self bootstrap components, Angular 5, made the bootstrap of the entire application only at the initialization stage, but now we can "compile" the components at any time the application runs from the HTML string.


This can be useful if we need to turn some HTML string from our API into a component.


But it will also be useful for writing a ComponentTranslateCompiler! Which can assemble components from our ngx-translate json `s.


That's all. Research and improve!


whole source


')

Source: https://habr.com/ru/post/358742/


All Articles