Guten Tag.
Es ist Zeit für ngx-translate Life Hacks. Anfangs habe ich 3 Teile geplant, aber weil der zweite Teil eigentlich nicht sehr informativ ist, werde ich versuchen, den zweiten Teil so kurz wie möglich zusammenzufassen.
Teil 1
AppTranslateLoader
Sie AppTranslateLoader
in Betracht, um TranslateHttpLoader
zu ersetzen. Unser AppTranslateLoader
achtet zunächst auf die Browsersprache und enthält Fallback-Logik, importiert MomentJs-Lokalisierungen und lädt über APP_INITIALIZER. Als Ergebnis der Kombination von zwei Teilen von Life-Hacks werden wir uns auf dem Weg mit der Schaffung einer bequemen und flexiblen Lokalisierungsinfrastruktur im Projekt befassen.
Das Hauptziel ist nicht AppTranslateLoader
(weil es recht einfach und nicht schwer zu AppTranslateLoader
ist), sondern die Schaffung einer Infrastruktur.
Ich habe versucht, so zugänglich wie möglich zu schreiben, aber da der Artikel viele Dinge enthält, die detaillierter beschrieben werden können, wird dies viel Zeit in Anspruch nehmen und für diejenigen, die bereits wissen, wie es geht, nicht interessant sein. Daher war der Artikel für Anfänger nicht sehr freundlich. Auf der anderen Seite gibt es am Ende einen Link zum Beispiel prodg.
Bevor ich anfange, möchte ich darauf hinweisen, dass es nicht nur möglich ist, Sprachen über http herunterzuladen, sondern auch einen Loader so zu schreiben, dass er die erforderlichen Sprachen in der Assembly-Phase in unser Bundle lädt. Sie müssen also keine Loader über http hinzufügen. Bei diesem Ansatz müssen Sie die Anwendung jedoch jedes Mal neu erstellen, wenn wir unsere Dateien mit Lokalisierungen ändern. Dies kann auch die Größe des .js-Bundles erheblich erhöhen.
// webpack-translate-loader.ts import { TranslateLoader } from '@ngx-translate/core'; import { Observable } from 'rxjs/Observable'; export class WebpackTranslateLoader implements TranslateLoader { getTranslation(lang: string): Observable<any> { return Observable.fromPromise(System.import(`../assets/i18n/${lang}.json`)); } }
Wenn die IDE auf System
schwört System
müssen Sie sie zu typings.d.ts hinzufügen:
declare var System: System; interface System { import(request: string): Promise<any>; }
Jetzt können wir WebpackTranslateLoader
in app.module verwenden:
@NgModule({ bootstrap: [AppComponent], imports: [ TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: WebpackTranslateLoader } }) ] }) export class AppModule { }
AppTranslateLoader
Beginnen wir AppTranslateLoader
mit dem Schreiben unseres AppTranslateLoader
. Zunächst möchte ich einige Probleme identifizieren, die mit dem Standard- TranslateHttpLoader
:
Flackern übersetzen. TranslateHttpLoader
weiß nicht, wie es als Teil des Anwendungsinitialisierungsprozesses ausgeführt werden soll, und wir können in eine Situation geraten, in der wir nach der Initialisierung feststellen, dass wir den Platz für die richtigen Beschriftungen in der Anwendung haben - die Schlüssel (MY_BUTTON_KEY ist der Platz für Meine Schaltfläche), die sich nach einem Moment in den richtigen Text ändern.
Termine Es wäre schön, einen Dienst zu haben, der die Lokalisierung von Daten ändert. Wenn Sie Text lokalisieren möchten, müssen Sie sich höchstwahrscheinlich um die Lokalisierung von Datum, Uhrzeit usw. kümmern. Sie können momentJs oder die in Angular integrierte i18n- Lösung verwenden. Beide Lösungen sind gut und verfügen über Angular 2+ -Pipes zum Formatieren in Ansichten.
- Caching. Mit
TranslateHttpLoader
müssen Sie Ihren FE-Server so konfigurieren, dass Ihre JSON-Bundles korrekt zwischengespeichert werden. Andernfalls sehen Benutzer alte Versionen der Lokalisierung, schlimmer noch, sie sehen Lokalisierungsschlüssel (wenn nach dem Zwischenspeichern durch den Benutzer neue hinzugefügt wurden). Ich möchte mich nicht jedes Mal darum kümmern, wenn ich auf einem neuen Server bereitstelle, wenn das Caching eingerichtet wird. Also werden wir Webpack dazu bringen, alles für uns zu tun, so wie es für .js-Bundles tut.
AppTranslateLoader-Entwurf
Lösungen für Probleme:
1. AppTranslateLoader
übersetzen - AppTranslateLoader
als Teil von APP_INITIALIZER
APP_INITIALIZER war auch aktiv an einem Artikel über Aktualisierungstoken beteiligt , wenn auch nicht geschmackvoll über Initialisierer. Ich empfehle Ihnen, den Artikel zu lesen, obwohl es sich um Aktualisierungstoken handelt. Tatsächlich ist die Entscheidung, Initialisierer zu verwenden, sehr offensichtlich (für diejenigen, die einen Initialisierer haben), aber ich hoffe trotzdem, dass es Leute gibt, die sich als nützlich erweisen:
//app.module.ts export function translationLoader(loader: AppTranslateLoader) { return () => loader.loadTranslation(); } @NgModule({ bootstrap: [AppComponent], providers: [ { provide: APP_INITIALIZER, useFactory: translationLoader, deps: [AppTranslateLoader], multi: true } ] }) export class AppModule { }
2. Das Problem der Daten. Wir werden die Sprache in Kürze zusammen mit ngx-tranlate wechseln.
Hier ist alles einfach - nachdem json mit Lokalisierung geladen wurde, schalten wir die Lokalisierung einfach auf momentJs (oder i18n) um.
Es ist auch erwähnenswert, dass momentJs wie i18n Lokalisierungen separat importieren können, momentJs können auch ein Bundle importieren, aber das gesamte Bündel von Lokalisierungen benötigt ~ 260 KB, und Sie benötigen nur 2 davon.
In diesem Fall können Sie nur zwei davon direkt in die Datei importieren, in der AppTranslateLoader
deklariert ist.
import 'moment/locale/en-gb'; import 'moment/locale/ru';
Jetzt befinden sich die Lokalisierungen en-gb und ru im js-Anwendungspaket. In AppTranslateLoader
können Sie einen frisch geladenen Sprachhandler hinzufügen:
export Class AppTranslateLoader { // .... private onLangLoaded(newLang: string) { // if (this.loadedLang && this.loadedLang !== newLang) { this.translate.resetLang(this.loadedLang); } this.loadedLang = newLang; this.selectedLang = newLang; // TODO: // , // en ru, momentJs en. moment().locale(newLang); // . momentJs localStorage.setItem(this.storageKey, newLang); // ls this.loadSubj.complete(); // - . }
!!! Dieser Handler hat einen Nachteil: Wenn in unserem Projekt nur die en-Lokalisierung für ngx-translate bereitgestellt wird, aber zum Beispiel momentan oder en oder gb verwendet werden muss, muss die Logik des Handlers erweitert werden, oder es sollte auch die en-Lokalisierung von en-gb bereitgestellt werden ngx-translate.
!!! Im Moment mit // TODO: Wir können ein Webpack-Plugin schreiben. Wir werden später einige Plugins in Betracht ziehen, aber das habe ich noch nicht.
Sie fragen, warum es unmöglich ist, Lokalisierungen von Datum und Uhrzeit sowie Lokalisierungen von Text in die Benutzeroberfläche zu laden (dynamisch über HTTP)? Dies liegt daran, dass Datumslokalisierungen ihre eigene Logik enthalten und daher als Javascript- Code dargestellt werden.
Trotzdem gibt es eine Möglichkeit, solche Lokalisierungen zu laden, indem Sie einen kleinen "schmutzigen" Code schreiben. Ich verwende diesen Code nicht in der Produktion, aber die zweite Lokalisierung in meinem Bundle stört mich nicht. Wenn Sie jedoch viele Lokalisierungen haben, möchten Sie diese dynamisch und nicht sehr sicher laden. Beachten Sie Folgendes:
private async loadAngularCulture(locale) { let angularLocaleText = await this.httpClient.get(`assets/angular-locales/${locale}.js`).toPromise();
Das letzte Mal habe ich diese Methode in Angular 4 getestet. Höchstwahrscheinlich funktioniert sie jetzt.
Leider funktioniert ein solcher Dirty-Life-Hack bei c momentJs nicht (nur Angular-Lokalisierung). Zumindest konnte ich keinen Weg finden, dies zu tun, aber wenn Sie ein sehr bärtiger Hacker-Programmierer sind, werde ich mich freuen, die Lösung in den Kommentaren zu sehen.
3. Caching. Ähnlich wie beim Erstellen eines .js-Bundles können Sie dem Namen des .json-Bundles einen Hash hinzufügen.
Es hängt alles davon ab, wie genau Sie alle json'es in einer Datei sammeln. Vielleicht haben Sie einfach alles in einer Datei. Im Internet finden Sie eine Reihe von npm-Modulen, die kleine json in einer Datei sammeln können. Ich habe nicht diejenigen gefunden, die an einen Hash anhängen und alles in einer Datei sammeln können. Webpack selbst kann auch nicht mit json umgehen, wie es die ngx-translate-Besonderheiten erfordern. Deshalb werden wir unser Webpack-Plugin schreiben.
Kurz gesagt: Wir müssen alle json im Projekt nach einem bestimmten Muster sammeln, während wir sie nach Namen (en, ru, de usw.) gruppieren müssen, da sich en.json beispielsweise in verschiedenen Ordnern befinden kann. Dann müssen Sie für jede gesammelte Datei einen Hash anhängen.
Hier gibt es ein Problem. Wie erkennt AppTranslateLoader
Dateinamen, wenn jede Lokalisierung einen eigenen Namen hat? Wenn Sie beispielsweise das Bundle in index.html aufnehmen, können Sie das HtmlWebpackPlugin einfügen und es auffordern , ein Skript-Tag mit dem Namen des Bundles selbst hinzuzufügen.
Um dieses Problem für .json-Lokalisierungen zu lösen, erstellt unser Webpack-Plugin config.json, das die Zuordnung des Sprachcodes zum Hash-Dateinamen enthält:
{ "en": "en.some_hash.json", "ru": "ru.some_hash.json" }
config.json wird ebenfalls vom Browser zwischengespeichert, aber es wird einige Zeit dauern, und wir können einfach einen zufälligen queryString-Parameter angeben, wenn GET mit dieser Datei überwachsen ist (wodurch sie ständig neu geladen wird). Oder weisen Sie config.json eine zufällige ID zu (ich werde diese Methode beschreiben, die erste finden Sie in Google).
Ich möchte auch die Infrastruktur und Atomizität von Lokalisierungen ein wenig vereinfachen. json mit Lokalisierung liegt im Ordner mit seiner Komponente. Um doppelte Schlüssel zu vermeiden, wird die Struktur des JSON-Bundles basierend auf dem Pfad zu einer bestimmten JSON-Datei erstellt. Zum Beispiel haben wir zwei en.json, eine liegt auf dem Pfad src/app/article-component
und die andere src/app/comment-component
. Ich möchte den folgenden JSON in der Ausgabe erhalten:
{ "article-component": { "TITLE": "Article title" }, "comment-component": { "TITLE": "Comment title" } }
Wir können den Teil des Pfades verwerfen, den wir nicht benötigen, damit die Schlüssel in den Ansichten so kurz wie möglich sind.
!!! Es gibt einen Nachteil: Wenn Sie die Komponente in einem anderen Ordner ablegen, ändert sich der Lokalisierungsschlüssel.
Später werden wir einen weiteren Life-Hack in Betracht ziehen, mit dem wir in der Komponente nur das letzte Schlüsselfeld angeben können, unabhängig davon, wo und wie tief unser Projekt im Projekt ist. Dementsprechend können wir es nach Belieben übertragen und nach Belieben umbenennen.
Grundsätzlich möchte ich eine Kapselung und sogar einen Hinweis auf Polymorphismus von ngx-translate-Lokalisierungen erreichen. Ich mag das Konzept der Kapselung von Ansichten in Angular - Angular View Encapsulation oder besser gesagt im Shadow DOM . Ja, dies vergrößert die Anwendung insgesamt, aber ich werde im Voraus sagen, dass die Arbeit mit Lokalisierungsdateien viel angenehmer geworden ist, nachdem ngx-translate stärker gekapselt wurde. Die Komponenten kümmerten sich nur noch um ihre Lokalisierungen. Außerdem können die Lokalisierungen in der untergeordneten Komponente abhängig von den Lokalisierungen in der übergeordneten Komponente neu definiert werden. Außerdem können Sie jetzt Komponenten von Projekt zu Projekt übertragen, die bereits lokalisiert sind. Aber wie anderswo gibt es Nuancen, dazu später mehr.
Fahren wir also mit unserem Plugin fort. Was ist das und wie? Plugin zum Zusammenführen von Lokalisierungen .
Den Quellcode für den Loader und das Plugin finden Sie unter dem Beispiellink am Ende des Artikels (Ordner ./build-utils).
Das Plugin macht alles mit dem, was oben geschrieben wurde, und akzeptiert die folgenden Optionen:
- weglassen. Namen im Pfad zur Lokalisierung, die ignoriert werden müssen (genau in diesem Moment möchte ich die zusätzlichen Teile des Pfads zur Datei entfernen).
- fileInput. regelmäßig zum Abrufen von Lokalisierungsdateien in prodge (wie Test in Webpack)
- rootDir. Wo soll ich nach der Datei nach dem fileInput-Muster suchen?
- outputDir. Hier werden die Konfigurationsdatei und die Lokalisierungen im Ordner dist erstellt
- Konfigurationsname. Unter welchem Namen wird die Konfigurationsdatei erstellt?
In meinem Projekt ist das Plugin folgendermaßen verbunden:
In Komponenten, die lokalisiert werden müssen, befindet sich ein @translations
Ordner, der en.json, ru usw. enthält.
Infolgedessen wird beim Umblättern alles in einer Datei zusammengefasst, wobei der Pfad zum Ordner @translations
berücksichtigt wird. Das Lokalisierungspaket befindet sich in dist / langs / und die Konfiguration wird als config. $ {Some-random} .json bezeichnet.
Als Nächstes stellen wir sicher, dass das gewünschte Lokalisierungspaket in die Anwendung geladen wird. Es gibt einen fragilen Punkt: Nur das Webpack kennt den Pfad zu den Lokalisierungen und den Namen der Konfigurationsdatei. Berücksichtigen Sie dies, damit die neuesten Daten in AppTranslateLoader eingehen und die Namen nicht an zwei Stellen geändert werden müssen.
// some inmports // ... // momentJs import * as moment from 'moment'; import 'moment/locale/en-gb'; import 'moment/locale/ru'; @Injectable() export class AppTranslateLoader { // public additionalStorageKey: string = ''; private translationsDir: string; private translationsConfig: string; private selectedLang: string; private fallbackLang: string; private loadedLang: string; private config: { [key: string]: string; } = null; private loadSubs = new Subscription(); private configSubs = new Subscription(); private loadSubj = new Subject(); private get storageKey(): string { return this.additionalStorageKey ? `APP_LANG_${this.additionalStorageKey}` : 'APP_LANG'; } constructor(private http: HttpClient, private translate: TranslateService) { // webpack // . this.translationsDir = `${process.env.TRANSLATE_OUTPUT}`; this.translationsConfig = `${process.env.TRANSLATE_CONFIG}`; this.fallbackLang = 'en'; const storedLang = this.getUsedLanguage(); if (storedLang) { this.selectedLang = storedLang; } else { this.selectedLang = translate.getBrowserLang() || this.fallbackLang; } } }
process.env.TRANSLATE_OUTPUT
funktioniert einfach nicht, wir müssen ein anderes Plugin im Webpack deklarieren (DefinePlugin oder EnvironmentPlugin):
Jetzt können wir den Pfad zu Lokalisierungen und den Namen der Konfiguration an nur einer Stelle ändern.
Standardmäßig können Sie vom Standard-Angular-Verkauf, der in der Webpack-Assembly generiert wird ( ng eject
eject), process.env.someValue nicht aus dem Code angeben (selbst wenn Sie DefinePlugin verwenden), der Compiler kann schwören. Damit dies funktioniert, müssen Sie 2a Bedingungen erfüllen:
- Fügen Sie in main.ts die erste Zeile
/// <reference types="node"/>
- package.json muss
@types/node
- npm install --save-dev @types/node
.
Wir fahren direkt mit dem Startvorgang fort.
Wenn Sie APP_INITIALIZER verwenden möchten, geben Sie Promise und nicht Observable zurück. Unsere Aufgabe ist es, eine Abfragekette zu schreiben:
- Zuerst müssen Sie config.json herunterladen (nur wenn nicht geladen).
- Versuchen Sie, die Sprache zu laden, die die Sprache des Browsers des Benutzers ist
- Stellen Sie die Fallback-Logik mit der Standard-Download-Sprache bereit.
// imports @Injectable() AppTranslateLoader { // fields ... // , // , // Subscription unsubscribe // private loadSubs = new Subscription(); private configSubs = new Subscription(); // - // Subject private loadSubj = new Subject(); // constructor ... // Promise! public loadTranslation(lang: string = ''): Promise<any> { if (!lang) { lang = this.selectedLang; } // if (lang === this.loadedLang) { return; } if (!this.config) { this.configSubs.unsubscribe(); this.configSubs = this.http.get<Response>(`${this.translationsDir}${this.translationsConfig}`) .subscribe((config: any) => { this.config = config; this.loadAndUseLang(lang); }); } else { this.loadAndUseLang(lang); } return this.loadSubj.asObservable().toPromise(); } private loadAndUseLang(lang: string) { this.loadSubs.unsubscribe(); this.loadSubs = this.http.get<Response>(`${this.translationsDir}${this.config[lang] || this.config[this.fallbackLang]}`) .subscribe(res => { this.translate.setTranslation(lang, res); this.translate.use(lang).subscribe(() => { this.onLangLoaded(lang); }, // fallback ngx-translate (err) => this.onLoadLangError(lang, err)); }, // fallback http (err) => this.onLoadLangError(lang, err)); } private onLangLoaded(newLang: string) { // if (this.loadedLang && this.loadedLang !== newLang) { this.translate.resetLang(this.loadedLang); } this.loadedLang = newLang; this.selectedLang = newLang; // TODO: // , // en ru, momentJs en. moment().locale(newLang); // . momentJs localStorage.setItem(this.storageKey, newLang); // ls this.loadSubj.complete(); // - . } private onLoadLangError(langKey: string, error: any) { // , if (this.loadedLang) { this.translate.use(this.loadedLang) .subscribe( () => this.onLangLoaded(this.loadedLang), (err) => this.loadSubj.error(err)); // } else if (langKey !== this.fallbackLang) { // fallback this.loadAndUseLang(this.fallbackLang); } else { // this.loadSubj.error(error); } }
Fertig.
Nun zurück zum Problem des Verschiebens von Komponenten in andere Ordner, der Kapselung und der Ähnlichkeit des Polymorphismus.
Tatsächlich haben wir bereits eine Art Kapselung. Lokalisierungen werden in Ordner neben den Komponenten verschoben. Alle Schlüsselpfade sind eindeutig. Wir können jedoch die Schlüssel der Komponente some-component1 in some-component2 lokalisieren, und es wird schwierig sein, alles im Auge zu behalten. Wir werden es später herausfinden.
<some-component1 [someLabel]="'components.some-component2.some_key' | tanslate"></some-component1> // components.some-component2 -
In Bezug auf die Bewegung von Bauteilen:
Der Schlüssel, den wir in der Ansicht verwenden, ist nun fest an den relativen Pfad zur Lokalisierungsdatei gebunden und hängt von der spezifischen Infrastruktur des Projekts ab.
Ich werde einen ziemlich traurigen Fall dieser Situation geben:
<div translate="+lazy-module.components.article-component.article_title"></div>
Was aber, wenn ich den Namen des Komponentenordners in Post-Component ändere?
Es wird ziemlich schwierig sein, diesen Schlüssel an allen erforderlichen Stellen einzugeben. Natürlich hat niemand das Kopieren, Einfügen und Ersetzen abgebrochen, aber das Schreiben ohne die Eingabeaufforderungen der IDE ist auch stressig.
Um diese Probleme zu lösen, achten wir darauf, was das Webpack dagegen unternimmt. Webpack hat so etwas wie einen Loader . Es gibt viele Loader, die mit Dateipfaden arbeiten: Zum Beispiel Ressourcenpfade in CSS - dank Webpack können wir relative Pfade für Hintergrundbilder angeben: url (../ relative.png) und so weiter Der Rest der Dateipfade im Projekt ist überall!
Wer auch immer seine Webpack-Builds erstellt hat, weiß, dass der Loader an der Eingabe eine Datei empfängt, die einem bestimmten Muster entspricht. Die Aufgabe des Loaders selbst besteht darin, diese Eingabedatei irgendwie zu transformieren und für weitere Änderungen durch andere Loader zurückzugeben.
Deshalb müssen wir unseren Loader schreiben. Die Frage ist, welche Art von Dateien werden wir ändern: Ansichten oder Komponenten? Auf einer Seite können Ansichten direkt in der Komponente und separat sein. Ansichten können groß genug und schwer zu analysieren sein. Stellen Sie sich vor, wir haben eine Ansicht, in der 100 Direktiven übersetzen (nicht in einer Schleife):
<div id="1">{{'./some_key_1' | translate}}</div> ... <div id="100">{{'../another_key_!' | translate}}</div>
Über Loader können wir den Schlüsselpfad zu Komponentenlokalisierungen in der Nähe jeder Pipe oder Direktive ersetzen.
<div id="1">{{'app.some-component.some_key_1' | translate}}</div>
Wir können einer Komponente, die die Lokalisierung ermöglicht, ein Feld hinzufügen:
@Component({ selector: 'app-some', template: '<div>{{(localization + 'key') | tanslate}}</div>' }) export class SomeComponent { localization = './' }
Es ist auch schlecht - Sie müssen überall einen Lokalisierungsschlüssel erstellen.
Da die offensichtlichsten Optionen schlecht aussehen, verwenden Sie einen Dekorator und speichern Sie einige Metadaten im Prototyp der Komponente (wie bei Angular).

Anmerkungen - Metadaten für Winkeldekorateure
__app_annotations__ - Metadaten, die wir für uns selbst speichern
Der Pfad zum Lokalisierungsordner relativ zur Komponente kann in den Dekorator geschrieben werden. Der gleiche Dekorator kann mit anderen Optionen als dem Pfad erweitert werden.
//translate.service.ts const app_annotations_key = '__app_annotations__'; export function Localization(path: string) { // tslint:disable-next-line:only-arrow-functions return function (target: Function) { const metaKey = app_annotations_key; Object.defineProperty(target, metaKey, { value: { // path. path, name: 'Translate' } } as PropertyDescriptor); }; } //some.component.ts @Component({...}) @Localization({ path: './', otherOptions: {...} }); export class SomeComponent { }
webpack, loader , - . , ( styleUrls) . loader, npm . .
, -. , -.
<div>{{'just_key' | translate}}</div>
. , , , . — Injector, . , Injector, '' , translate . Injector, ( ), 'get'.

, parent , , Injector'a , , , , , .
, API, forwarRef() ( Angular reactive forms, control ). , . .
// translate.service.ts export const TRANSLATE_TOKEN = new InjectionToken('MyTranslateToken'); // app.component.ts @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [{provide: TRANSLATE_TOKEN, useExisting: forwardRef(() => AppComponent)}] }) @Localization('./') export class AppComponent { title = 'app'; }
, , , forwardRef().
, Injector forwardRef() , . , '' . , , .
// my-translate.directive.ts @Directive({ // tslint:disable-next-line:directive-selector selector: '[myTranslate]' }) export class MyTranslateDirective extends TranslateDirective { @Input() public set myTranslate(e: string) { this.translate = e; } private keyPath: string; constructor(private _translateService: TranslateService, private _element: ElementRef, _chRef: ChangeDetectorRef, // forwardRef() @Inject(TRANSLATE_TOKEN) @Optional() protected cmp: Object) { super(_translateService, _element, _chRef); // const prototype = Object.getPrototypeOf(cmp || {}).constructor; if (prototype[app_annotations_key]) { // this.keyPath = prototype[app_annotations_key].path; } } public updateValue(key: string, node: any, translations: any) { if (this.keyPath) { // , // key = `${this.keyPath.replace(/\//, '.')}.${key}`; } super.updateValue(key, node, translations); } }
.
- :
<div>{{'just_this_component_key' | myTranslate}}</div> // <div myTranslate="just_this_component_key"></div>
translate , . , , - :
//en.bundle.json { "global_key": "Global key" "app-component": { "just_key": "Just key" } } //some-view.html <div translate="global_key"></div>
Research and improve!
full example
:
- FE node.js stacktrace.js.
- Jest Angular .
- Web worker ) , , Angular .