角度:ngx-translate。 使用Webpack改善基础架构

大家好


现在是时候进行ngx转换生活技巧了。 最初,我计划了3个部分,但是由于第二部分实际上不是非常有用-在此,我将尝试尽可能简要地总结第二部分。


第一部分


考虑用AppTranslateLoader代替TranslateHttpLoader 。 我们的AppTranslateLoader首先会注意浏览器语言,并包含回退逻辑,导入MomentJs本地化并通过APP_INITIALIZER进行加载。 此外,由于结合了生活中的两部分内容,因此,我们将深入研究在项目中创建方便且灵活的本地化基础结构。


主要目标不是AppTranslateLoader (因为它很简单,而且制作起来并不困难),而是创建基础结构。


我试图写尽可能容易访问的东西,但是因为本文有很多内容可以更详细地描述-这将花费很多时间,并且对于那些已经知道如何做的人来说不会很有趣。 因此,本文对初学者不是很友好。 另一方面,最后有一个指向示例产品的链接。


在开始之前,我想指出的是,除了通过http下载语言之外,还可以编写一种加载器,以便在组装阶段将必要的语言加载到我们的捆绑软件中。 因此,您不需要通过http添加任何加载器,但是,另一方面,使用这种方法,每次我们使用本地化更改文件时,都将需要重新构建应用程序,而且,这会大大增加.js包的大小。


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

如果IDE在System发誓System需要将其添加到types.d.ts中:


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

现在我们可以在app.module中使用WebpackTranslateLoader:


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

AppTranslateLoader


因此,让我们开始编写AppTranslateLoader 。 首先,我想确定使用标准TranslateHttpLoader必须遇到的几个问题:


  • 翻译闪烁。 TranslateHttpLoader不知道如何在应用程序初始化过程中执行,并且当初始化后我们发现我们在应用程序中拥有正确标签的位置时-按键(MY_BUTTON_KEY是我的按钮的位置),稍后会变为正确的文本。


  • 日期 拥有一个可以切换日期本地化的服务会很好。 在本地化文本时,最有可能需要照顾本地化日期,时间等。 您可以使用momentJs或Angular内置的i18n解决方案。 两种解决方案都不错,并且具有用于视图格式的Angular 2+管道。


  • 正在缓存。 使用TranslateHttpLoader ,您必须配置FE服务器以正确地缓存json包。 否则,用户将看到旧版本的本地化,更糟的是,他们将看到本地化密钥(如果用户缓存后添加了新的本地化密钥)。 我不想在每次设置新服务器时都在设置缓存的时候打扰。 因此,我们将使Webpack像处理.js捆绑包一样为我们做所有事情。

AppTranslateLoader草案


解决问题的方法:

1.翻译闪烁问题-将AppTranslateLoader用作AppTranslateLoader一部分

APP_INITIALIZER还积极参与了有关刷新令牌的文章,即使对初始化器没有品味,我仍然建议您阅读该文章,尽管它与刷新令牌有关。 实际上,对于拥有初始化器的人来说,使用初始化器的决定非常明显,但我仍然希望有人能派上用场:


 //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.日期问题。 我们只需要在瞬间与ngx-tranlate一起切换语言即可。

这里的一切都很简单-加载具有本地化的json后,我们只需将本地化切换为momentJs(或i18n)即可。


还值得注意的是,momentJs可以像i18n一样分别导入本地化,momentJs也可以导入捆绑包,但是整个捆绑包大约需要260KB,您只需要其中两个即可。


在这种情况下,您只能直接在声明AppTranslateLoader的文件中导入其中的2个。


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

现在,en-gb和ru本地化将在js应用程序包中。 在AppTranslateLoader您可以添加新加载的语言处理程序:


 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本地化,但是例如在瞬间或需要使用en或en-gb时,则必须扩展处理程序的逻辑,或者还应在内部提供en-gb的en-localization ngx-translate。


!!! // TODO:目前,我们可以编写一个webpack插件,稍后我们将考虑几个插件,但是我还没有。


您问,为什么不可能在接口中加载日期和时间的本地化以及文本的本地化(通过HTTP动态)? 这是因为日期本地化包含其自身的逻辑,因此以javascript代码形式显示。


但是尽管如此,还是有一种方法可以通过编写一些“脏”代码来加载这样的本地化。 我没有在生产中使用此代码,但是捆绑包内的第二个本地化版本并不困扰我。 但是,如果您有许多本地化,则要动态地而不是非常安全地加载它们,请记住:


 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捆绑包名称中添加哈希值。

这完全取决于您如何将所有json准确地收集到一个文件中,也许您只是将所有内容都存储在一个文件中。 在Internet上,您可以找到许多可以在一个文件中收集小型json的npm模块。 我没有找到可以附加到哈希表并将所有内容收集到一个文件中的文件。 Webpack本身也无法按照ngx-translate规范的要求处理json。 因此,我们将编写我们的webpack插件。


简而言之:我们需要根据特定模式收集项目中的所有json,而我们需要按名称(en,ru,de等)将它们分组,因为例如en.json可以位于不同的文件夹中。 然后,对于每个收集的文件,您都需要附加一个哈希。


这里有问题。 如果每个本地化都有自己的名称, AppTranslateLoader如何识别文件名? 例如,将捆绑软件包含在index.html中,我们可以包括HtmlWebpackPlugin,并要求它添加一个带有捆绑软件名称的脚本标签。


为了解决.json本地化的问题,我们的webpack插件将创建config.json,其中将包含语言代码与哈希文件名的关联:


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

config.json也将由浏览器缓存,但是会花费一些时间,我们可以在GET扩展此文件时简单地指定一个随机的queryString参数(从而不断重新加载它)。 或为config.json分配一个随机ID(我将描述此方法,第一个方法可以在Google中找到)。


我还想简化本地化的基础架构和原子性。 具有本地化功能的json将位于包含其组件的文件夹中。 并且为了避免重复的密钥,将基于特定json文件的路径构建json包的结构。 例如,我们有两个en.json,一个位于路径src/app/article-component ,另一个位于src/app/comment-component 。 我想在输出中获取以下json:


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

我们可以丢弃不需要的路径部分,以使键在视图中尽可能短。


!!! 有一个缺点:当您将组件放置在另一个文件夹中时,本地化密钥将会更改。


稍后,我们将考虑另一个生活技巧,无论我们在项目中的位置和深度如何,都可以在组件中仅指示最后一个关键字段,因此,我们可以根据需要进行传输并根据需要进行重命名。


基本上,我想实现封装,甚至实现ngx-translate本地化的多态性提示。 我喜欢用Angular- Angular View EncapsulationShadow DOM封装视图的概念。 是的,这增加了整个应用程序的大小,但是我先要说,在ngx-translate被更加封装之后,使用本地化文件变得更加愉快。 组件开始只关心它们的本地化,此外,有可能根据父组件中的本地化重新定义子组件中的本地化。 另外,现在您可以在项目之间转移组件,并且它们已经本地化。 但是与其他地方一样,在以后会有更多细微差别。


因此,让我们继续我们的插件。 这是什么以及如何合并本地化插件
加载程序和插件的源代码可以在文章底部的示例链接(文件夹./build-utils)中找到。


该插件完成上述所有操作,并接受以下选项:


  • 省略。 本地化路径中的名称需要忽略(这正是我要删除文件路径多余部分的那一刻)
  • fileInput。 定期获取产品中的本地化文件(例如Webpack中的测试)
  • rootDir。 从哪里开始通过fileInput模式查找文件
  • outputDir。 配置文件和本地化将在dist文件夹中创建的位置
  • configName。 将以什么名称创建配置文件。

在我的项目中,插件通过以下方式连接:


 // 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等。


结果,翻转时,考虑到@translations文件夹的路径,所有内容都将收集到一个文件中。 本地化包将位于dist / langs /中,并且配置将被命名为config。$ {Some-random} .json。


接下来,我们将确保所需的本地化软件包已加载到应用程序中。 这是一个脆弱的时刻-只有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; } } } 

process.env.TRANSLATE_OUTPUT不能正常工作,我们需要在webpack中声明另一个插件(DefinePlugin或EnvironmentPlugin):


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

现在,我们只能在一个地方更改本地化路径和配置名称。
默认情况下,从Webpack程序集(ngject)中生成的默认Angular销售中,您无法从代码中指定process.env.someValue (即使您使用DefinePlugin),编译器也会发誓。 为了使其正常工作,您需要满足2a条条件:


  • 在main.ts中添加第一行/// <reference types="node"/>
  • package.json必须具有@types/node npm install --save-dev @types/node

我们直接进行引导过程。
如果打算使用APP_INITIALIZER,请确保返回Promise,而不是Observable。 我们的任务是编写一个查询链:


  • 首先,您需要下载config.json(仅在未加载的情况下)。
  • 尝试加载语言,这是用户浏览器的语言
  • 提供具有默认下载语言的后备逻辑。

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

但是,如果我将组件文件夹的名称更改为post-component怎么办?
在所有必要的位置都很难输入此密钥。 当然,没有人取消复制粘贴和查找替换,但是在没有IDE提示的情况下编写它也很麻烦。


为了解决这些问题,让我们注意webpack正在做什么呢? Webpack具有诸如loader之类的东西,有许多可用于文件路径的加载器 :例如,css中的资源路径-借助webpack,我们可以指定相对的背景图像路径:url(../ relative.png),等等项目中其余文件路径无处不在!


进行webpack构建的人都知道,加载程序会在输入中接收到与特定模式匹配的文件。 加载程序本身的任务是以某种方式转换此输入文件并返回它,以供其他加载程序进行进一步更改。


因此,我们需要编写我们的加载器。 问题是我们将更改哪种文件:视图或组件? 一方面,视图可以直接在组件中,也可以分别在组件中。 视图可能足够大且难以解析,请想象一下,如果我们有一个视图,其中包含100个翻译指令(不在循环中):


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

通过加载程序,我们可以将关键路径替换为每个管道或指令附近的组件本地化。


 <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一样)。


图片


注释 -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 { } 

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


:


  1. FE node.js stacktrace.js.
  2. Jest Angular .
  3. Web worker ) , , Angular .

Source: https://habr.com/ru/post/zh-CN413787/


All Articles