Хабрахабр

Angular: ngx-translate. Улучшаем инфраструктуру c помощью Webpack

Доброго времени суток.

Изначально я планировал 3 части, но т.к вторая часть на деле мало информативна — в этой постараюсь максимально кратко изложить 2е части. Пришло время ngx-translate лайфхаков.

Часть 1

Наш AppTranslateLoader будет в первую очередь обращать внимание на язык браузера и содержать fallback логику, импортировать локализации MomentJs, и производить загрузку через APP_INITIALIZER. Рассмотрим AppTranslateLoader в замену TranslateHttpLoader. Так же в результате объединения 2ух частей лайфхаков, по ходу мы углубимся в создание удобной и гибкой инфраструктуры локализаций в проекте.

Основной целью является не AppTranslateLoader (т.к он достаточно простой и не сделать его сложно), а создание инфраструктуры.

Потому статья вышла сильно не дружелюбной к новичкам. Я пытался писать максимально доступно, но т.к в статье достаточно много чего можно расписать подробнее — это займет много вермени и будет не интересно тем, кто уже умеет). С другой стороны в конце есть ссылка на expample продж.

Таким образом не нужно дописывать никаких лоадеров по http, но с другой стороны, с таким подходом нужно будет делать ребилд приложения каждый раз, когда мы меняем наши файлы с локализациями, а так же, это может сильно увеличить размер .js бандла. Перед тем как начать хочу обратить внимание, что кроме загрузки языков по http, есть возможность написать лоадер таким образом, что бы он загружал нужные языки в наш бандл еще на этапе сборки.

// webpack-translate-loader.ts
import 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`)); }
}

Если IDE ругается на System нужно добавить его в typings.d.ts:

declare var System: System;
interface System { import(request: string): Promise<any>;
}

Теперь мы можем использвовать WebpackTranslateLoader в app.module:

@NgModule({ bootstrap: [AppComponent], imports: [ TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: WebpackTranslateLoader } }) ]
})
export class AppModule { }

AppTranslateLoader

Для начала хочу обозначить несколько проблем с которыми придется столкнутся используя стандартный TranslateHttpLoader: Итак, приступим к написания нашего AppTranslateLoader.

  • Translate flickering. TranslateHttpLoader не умеет выполняется в рамках процесса инициализации приложения и мы можем попасть в ситуацию когда после инициализации видим, что у нас в приложении место корректных лейблов — ключи (MY_BUTTON_KEY место My button), которые спустя мгновение меняются на корректный текст.

  • Когда речь идет о локализации текста, скорее всего вам придется позаботится и о локализации дат, времени и т.д. Даты. Неплохо было бы иметь сервис переключающий локализацию дат. Оба решения хороши, и имеют Angular 2+ пайпы для форматирования во вьюшках. Вы можете использовать momentJs или же встроенное в Angular решение i18n.

  • Кеширование. используя TranslateHttpLoader обязательно нужно настроить ваш FE сервер корректно кешировать ваши json бандлы. Иначе пользователи будут видеть старые версии локализации, хуже того они будут видеть ключи локализации (если были добавлены новые после кеширования юзером). Я не хочу каждый раз при деплое на новом сервере заморачиваться с моментом настройки кештрования. Значит сделаем так, что бы Webpack делал все за нас так, как он делает это для .js бандлов.

AppTranslateLoader draft

Решения проблем:

1. проблема translate flickering — использовать AppTranslateLoaderв рамках APP_INITIALIZER

На самом деле решение юзать initializer очень очевидное (для тех кто заком initializer), но все же надеюсь есть люди кому пригодится: APP_INITIALIZER так же активно был заюзан, в статье про refresh token, если не вкурсе про initializer — советую почитать статью несмотря на то, что там про refresh token.

//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. Проблема дат. Просто будем переключать язык в momentJs вместе c ngx-tranlate.

Здесь все просто — после того, как json с локализацией загружен, мы просто переключим локализацию в momentJs (или i18n).

Cтоит так же обратить внимание, что momentJs как и i18n может импортировать локализации отдельно, momentJs так же может импортировать и пачкой, но вся пачка локализаций занимает ~260KB, а вам допустим нужно только 2е из них.

В таком случае можно импортировать только 2е из них прямо в файле где объявлен AppTranslateLoader.

import 'moment/locale/en-gb';
import 'moment/locale/ru';

В AppTranslateLoader можно добавить обработчик свеже-загруженного языка: Теперь локализации en-gb и ru будут в js бандле приложения.

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(); // оповещаем подписчиков - все что нужно загружено и инициализировано.
}

этот обработчик имеен недостаток: В случае если у нас в проекте для ngx-translate предусмотрена только локализация en, а к примеру для momentJs нужно использовать или en или en-gb, логику обработчика придется расширить, или же предусмотреть локализацию en-gb и в рамках ngx-translate. !!!

для момента с // TODO: можно написать webpack плагин, парочку плагинов мы рассмотрим далее, но конретно этого у меня пока нет. !!!

Все потому, что локализации дат сожержат свою логику, и потому представлены в виде javascript кода. Вы спросите, а почему нельзя загружать локализации дат и времени так же как и локализации текста в интерфейсе (тобишь динамически, по HTTP)?

Я не использую этот код в продакшне, но меня не напрягает 2е локализации внутри моего бандла. Но несмотря на это есть способ загрузить подобные локализации, написав немного 'грязного' кода. Но если у вас много локализаций, хочется загрузить их динамически и не очень безпасно, имейте ввиду:

private async loadAngularCulture(locale) { let angularLocaleText = await this.httpClient.get(`assets/angular-locales/${locale}.js`).toPromise(); // extracting the part of the js code before the data, // and i didn't need the plural so i just replace plural by null. const startPos = angularLocaleText.indexOf('export default '); angularLocaleText = 'return ' + angularLocaleText.substring(startPos + 15).replace('plural', null); // The trick is here : to read cldr data, i use a function const f = new Function(angularLocaleText); const angularLocale = f(); // console.log(angularLocale); // And now, just registrer the object you just created with the function registerLocaleData(angularLocale);
}

Скорее всего и сейчас он рабочий. Последний раз я тестировал этот способ в Angular 4.

По крайней мере у меня не получилось найти способа это сделать, но если вы очень бородатый хакер программист — буду рад увидеть решение в комментариях. К сожалению подобный 'грязный' лайфхак не сработает в случае c momentJs (только Angular локализации).

3. Кеширование. Подобно сборке .js бандла можно добавить к имени .json бандла хеш.

В просторах интерета можно найти некоторое количество npm модулей которые умеют собирать мелкие json'ки в один фалй. Здесь все зависит от того, как именно вы собираете все json'ки в один файл, возможно у вас просто все лежит в одном файле. Сам webpack тоже не может обработать json как этого требует специфика ngx-translate. Я не нашёл тех, которые смогут и приделать к имеши хеш и собрать все в один файл. Потому мы напишем свой webpack плагин.

Затем к каждому собранному файлу нужно приделать хеш. Коротко говоря: нам нужно собрать все json в прокте по определенному паттерну, при этом нужно сгрупировать их по имени (en,ru,de и т.д) т.к в разных папках может лежать к примеру en.json.

Как AppTranslateLoader узнает имена файлов если у каждой локализации будет собственное имя? Здесь есть проблема. Например включая бандл в index.html мы можем подключить HtmlWebpackPlugin и попросить его самостоятельно добавить script тег с указанием имени бандла.

Что бы решить эту проблему для .json локализаций наш webpack плагин будет создавать config.json, в котором будет содержаться ассоциация кода языка к имени файла с хешем:

{ "en": "en.some_hash.json", "ru": "ru.some_hash.json"
}

Или же приписать к config.json рандомный ID (я опишу этот способ, первый можно найти в гугле). config.json так же будет закеширован браузером но занимает он мало и мы можем просто при GET заросе этого файла указать рандомный queryString параметр (таким образом постоянно загружая его заново).

json с локализацией будет лежать в папке со своим компонентом. Так же я хочу немного упростить инфраструктуру и атомарность локализаций. Например у нас есть два en.json, один лежит по пути src/app/article-component, а другой src/app/comment-component. И во избежание дубликатов ключей, структрура json бандла будет строится на основе пути к конкретному json файлу. На выходе хочу получить вот такой json:

{ "article-component": { "TITLE": "Article title" }, "comment-component": { "TITLE": "Comment title" }
}

Мы можем отбросить часть пути которая нам не нужна, что бы во вьюшках ключи были максимально короткими.

Здесь есть недостаток: при меремещении компонента в другую папку у нас поменяется ключ локализации. !!!

Позже мы рассмотрим еще один лайфхак который позволит нам указывать в компоненте только последнее поле ключа независимо от того, где и как глубоко в проекте наш компонент, и соответственно мы сможет переносить его как угодно и переименовывать как угодно.

Мне нравится концепция инкапсуляции вьюшек в Angular — Angular View Encapsulation, а если быть точнее Shadow DOM. Принципиально, я хочу добится инкапсуляции и даже некого намека на полиморфизм ngx-translate локализаций. Компоненты стали заботится только о своих локализациях, кроме того можно будет переопределять локализации в дочернем компоненте в зависимости от локализаций в родительском компоненте. Да, это увеличивает размер приложения в целом, но скажу наперед, после того, как ngx-translate стал более инкапсулированным, работать с файлами локализаций стало намного приятнее. Но как и везде есть нюансы, об этом позже. Так же, теперь можно переносить компоненты из проекта в проект, и они уже будут локализованы.

Что это и как. Итак перейдем к нашему плагину. merge localizations plugin.
Исходники лоадера и плагина можно найти по ссылке на example в самом низу статьи (папка ./build-utils).

Плагин делает все о чем написано выше, и принимает следующие опции:

  • omit. имена в пути к локализации которые нужно игнорировать (это именно тот самый момент, где я хочу убарть лишние части пути к файлу)
  • fileInput. регулярка для выборки файлов локализаций в продже (как test в webpack)
  • rootDir. откуда начинать искать фалы по паттерну fileInput
  • outputDir. где в папке dist будут созданы config файл и локализации
  • configName. под каким именем будет создан config файл.

В моем проекте плагин подключен таким образом:

// build-utils.js
// part of METADATA { // ... translationsOutputDir: 'langs/', translationsFolder: '@translations', translationsConfig: `config.${Math.random().toString(36).substr(2, 9)}.json`,
} //webpack.common.js new MergeLocalizationPlugin({ fileInput: [`**/${METADATA.translationsFolder}/*.json`, 'app-translations/**/*.json'], rootDir: 'src', omit: new RegExp(`app-translations|${METADATA.translationsFolder}|^app`, 'g'), outputDir: METADATA.translationsOutputDir, configName: METADATA.translationsConfig
}),

внурти компонентов, которым нужна локализация имеется папка @translations, в ней лежат en.json, ru и т.д.

Бандл локализаций будет в dist/langs/, а конфиг будет назван как config.${некий-рандом}.json. В итоге при сбоке все будет собрано в один файл с учетом пути к папке @translations.

Тут есть хрупкий момент — про путь к локализациям и про имя config файла знает только webpack, давайте учем это, дабы в AppTranslateLoader попадали актуальные данные, и не было надобности менять имена в двух местах. Далее сделаем так, что бы нужный бандл локализации загружался в приложение.

// 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; } } }

TRANSLATE_OUTPUT просто так работать не будет, нам нужно в webpack объявить еще один плагин (DefinePlugin или EnvironmentPlugin): process.env.

// METADATA declaration
const METADATA = { translationsOutputDir: 'langs/', translationsFolder: '@translations', translationsConfig: `config. ${Math.random().toString(36).substr(2, 9)}.json`,
}; // complex webpack config... // webpack plugins... new DefinePlugin({ 'process.env.TRANSLATE_OUTPUT': JSON.stringify(METADATA.translationsOutputDir), 'process.env.TRANSLATE_CONFIG': JSON.stringify(METADATA.translationsConfig),
}),

Дабы это сработало нужно выполнить 2а условия: Теперь мы можем менять путь к локализациям и имя конфига только в одном месте.
По умолчанию из дефолтного Angular проджа сгенерированного в webpack сборку (ng eject), нельзя из кода указывать process.env.someValue (даже если использовать DefinePlugin), компилятор может ругатся.

  • в main.ts добавть 1-ую строку /// <reference types="node"/>
  • в package.json должен присутствовать @types/nodenpm install --save-dev @types/node.

Наша задача написать цепочку запросов: Перейдем непосредственно к процессу загрузки.
Если вы собираетесь использовать APP_INITIALIZER, обязательно возвращайте Promise, а не Observable.

  • Для начала необходимо загрузить config.json (только если не загружен).
  • попытаться загрузить язык, который является языком браузера юзера
  • предусмотреть fallback логику с заргузкой языка по умолчанию.

// 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); }
}

Готово.

Теперь вернемся к проблеме перемещения компонентов в другие папки, инкапсуляции и подобию полиморфизма.

Локализации распиханы по папкам рядом с компонентами, все пути-ключи уникальны, но мы все же можем локализовать ключи компонента some-component1 внутри some-component2 и за этим всем сложно будет уследить, позже мы с этим разберемся. По сути некую инкапсуляцию мы уже имеем.

<some-component1 [someLabel]="'components.some-component2.some_key' | tanslate"></some-component1>
// components.some-component2 - глобальный и доступен отовсюду

По поводу перемещения компонентов:
Сечас ключ который мы будем использовать во вьюшке жестко завязан на относительный путь к файлу локализации и зависит от конкретной инфраструктуры проекта.

Приведу достаточно печальный случай этой ситуации:

<div translate="+lazy-module.components.article-component.article_title"></div>

Конечно копипаст и find-replace никто не отменял, но и писать это без подсказок IDE тоже напряжно. А что если я поменяю имя папки компонента на post-component?
Довольно тяжело будет вписывать этот ключ во всех необходимых местах.

Webpack имеет такую вещь, как loader, в наличии есть много loaders, которые оперируют путями к файлам: например пути к ресурсам в css — благодаря webpack мы можем указывть относительные пути background-image: url(../relative.png), а так же остальные пути к файлам в проекте — они всюду! Для решения этих пороблем, обратим внимание на то, что по этому поводу предпринимает webpack?

Задача самого loader, неким образом трансформировать этот входной файл и вернуть его, для дальнейших изменений другими loaders. Кто делал свои webpack сборки, знает, что loader получает на входе файл который соответсвует некому паттерну.

Вопрос в том какие именно файлы мы будем менять: вьюшки или компоненты? Потому нам небходимо написать свой loader. Вьюшки могут быть достаточно большими и их сложно парсить, представим если у нас есть вьюшка где 100 translate директив (не в цикле): С одной стороны вьюшки могут быть прямо в компоненте и отдельно.

<div id="1">{{'./some_key_1' | translate}}</div>
...
<div id="100">{{'../another_key_!' | translate}}</div>

можем через loader подставить путь-ключ к локализациям компонентов возле каждого пайпа или директивы.

<div id="1">{{'app.some-component.some_key_1' | translate}}</div>
// app.some-component. - будет подставлен loader'ом

можем добавить поле в компонент, который предоставляет локализацию:

@Component({ selector: 'app-some', template: '<div>{{(localization + 'key') | tanslate}}</div>'
})
export class SomeComponent { localization = './'
}

Так же плохо — придется везде составлять ключ локализации.

Т.к самые очевидные варианты выглядят плохо, попробуем использовать декоратор и сохранять некие метаданные в прототипе компонента (так как это делает Angular).

image

annotations — метаданные декораторов Angular
__app_annotations__ — метаданные которые мы будем хранить для себя

Путь к папке локализаций относительно компонента, можно записать в декоратор, этот же декоратор можно будет расширить и другими опциями кроме пути.

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

На самом деле момент с путем к локализациям не обязателен, просто это дает возможность (к примеру как и в случае styleUrls) указать путь к неким общим файлам. В итоге после сборки через webpack, наш loader обработает компоненты и декоратор будет знать про путь относительно корня проджа, а значит он будет знать путь-ключ к нужной локализации компонента. loader, я не добавлял в npm т.к он уж очень эксперементальеный.

Я хочу просто писать ключ во вьюхе, что бы пайп или дериектива локализации самостоятельно подставляла нужный путь-ключ. Итак у нас есть компонент с кастомным декоратором, который знает путь-ключ.

<div>{{'just_key' | translate}}</div>

Существует достаточно много способов, передать компонент в директиву или пайп, но мне хотелось бы такой способ, который позволит мне не делать это слишком явно и слишком часто. Теперь осталось только достать наши метаданные из прототипа инстанса компонента. Но по непонятным мне причинам Injector, хоть и имеет очень много данных (вот числе и родительский компонент), имеет при этом публичный интерфейс только с методом 'get'. Самое очевидно решение — Injector, т.к внутри каждого компонента, директивы или пайпа мы можем получить Injector, а каждый инжетор получает 'контекст' родителького инжектора, значит translate директива может достать из инжектора родителький компонент.

image

как видим, найти 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

В будущем есть следующие темы для просвещения:

  1. Пишем логгер ошибок для FE с красивыми трейсами на node.js с использованием stacktrace.js.
  2. Подключаем Jest к Angular проекту.
  3. Web worker костыли) Когда очень нужно запустить в воркере, то, что Angular не может.
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть