Hari yang baik
Sudah waktunya untuk ngx-menerjemahkan hacks kehidupan. Awalnya, saya merencanakan 3 bagian, tetapi karena bagian kedua sebenarnya tidak terlalu informatif - dalam hal ini saya akan mencoba meringkas bagian ke-2 sesingkat mungkin.
Bagian 1
Pertimbangkan AppTranslateLoader
untuk menggantikan TranslateHttpLoader
. AppTranslateLoader
kami pertama-tama akan memperhatikan bahasa browser dan berisi logika mundur, mengimpor pelokalan MomentJs, dan memuat melalui APP_INITIALIZER. Selain itu, sebagai hasil dari menggabungkan 2 bagian peretasan kehidupan, sepanjang perjalanan kita akan mempelajari penciptaan infrastruktur lokalisasi yang nyaman dan fleksibel dalam proyek ini.
Tujuan utama bukanlah AppTranslateLoader
(karena cukup sederhana dan tidak sulit dibuat), tetapi pembuatan infrastruktur.
Saya mencoba menulis semudah mungkin diakses, tetapi karena artikel itu memiliki banyak hal yang dapat dijelaskan secara lebih rinci - itu akan memakan banyak waktu dan tidak akan menarik bagi mereka yang sudah tahu caranya). Karena itu, artikel itu tidak terlalu ramah untuk pemula. Di sisi lain, pada akhirnya ada tautan ke expample prodg.
Sebelum memulai, saya ingin mencatat bahwa selain mengunduh bahasa melalui http, dimungkinkan untuk menulis loader sedemikian rupa sehingga memuat bahasa yang diperlukan ke dalam bundel kami pada tahap perakitan. Dengan demikian, Anda tidak perlu menambahkan pemuat apa pun untuk http, tetapi di sisi lain, dengan pendekatan ini, Anda harus membangun kembali aplikasi setiap kali kami mengubah file kami dengan pelokalan, dan juga, ini dapat sangat meningkatkan ukuran bundel .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`)); } }
Jika IDE bersumpah di System
Anda perlu menambahkannya ke typings.d.ts:
declare var System: System; interface System { import(request: string): Promise<any>; }
Sekarang kita dapat menggunakan WebpackTranslateLoader
di app.module:
@NgModule({ bootstrap: [AppComponent], imports: [ TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: WebpackTranslateLoader } }) ] }) export class AppModule { }
AppTranslateLoader
Jadi, mari mulai menulis AppTranslateLoader
kami. Untuk mulai dengan, saya ingin mengidentifikasi beberapa masalah yang harus dihadapi menggunakan standar TranslateHttpLoader
:
Terjemahkan berkedip. TranslateHttpLoader
tidak tahu bagaimana mengeksekusi sebagai bagian dari proses inisialisasi aplikasi dan kita dapat masuk ke dalam situasi ketika setelah inisialisasi kita melihat bahwa kita memiliki tempat untuk label yang benar dalam aplikasi - kunci (MY_BUTTON_KEY adalah tempat tombol Saya), yang setelah beberapa saat berubah ke teks yang benar.
Tanggal Akan menyenangkan untuk memiliki layanan yang mengubah lokalisasi tanggal. Ketika datang ke pelokalan teks, kemungkinan besar Anda harus mengurus tanggal, waktu, dll. Anda dapat menggunakan momentJs atau solusi i18n yang terintegrasi ke Angular. Kedua solusi baik, dan memiliki pipa Angular 2+ untuk memformat dalam pandangan.
- Caching. menggunakan
TranslateHttpLoader
, Anda harus mengonfigurasi server FE Anda untuk melakukan cache json bundel dengan benar. Jika tidak, pengguna akan melihat versi lokalisasi lama, lebih buruk lagi mereka akan melihat kunci lokalisasi (jika yang baru ditambahkan setelah caching oleh pengguna). Saya tidak ingin repot setiap kali saya menggunakan server baru dengan saat menyiapkan caching. Jadi kita akan membuat Webpack melakukan segalanya untuk kita seperti yang dilakukannya untuk bundel .js.
Draf AppTranslateLoader
Solusi untuk masalah:
1. terjemahkan masalah berkedip - gunakan AppTranslateLoader
sebagai bagian dari APP_INITIALIZER
APP_INITIALIZER juga aktif terlibat dalam sebuah artikel tentang penyegaran token , jika tidak berselera tentang penginisialisasi - saya menyarankan Anda untuk membaca artikel terlepas dari kenyataan bahwa ini adalah tentang penyegaran token. Sebenarnya, keputusan untuk menggunakan penginisialisasi sangat jelas (bagi mereka yang memiliki penginisialisasi), tetapi saya masih berharap ada orang yang berguna:
//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. Masalah tanggal. Kami hanya akan mengganti bahasa di momentJs bersama dengan ngx-tranlate.
Semuanya sederhana di sini - setelah json dengan pelokalan dimuat, kami hanya mengalihkan pelokalan ke momentJs (atau i18n).
Perlu juga dicatat bahwa momentJs, seperti i18n, dapat mengimpor pelokalan secara terpisah, momentJs juga dapat mengimpor bundel, tetapi keseluruhan bundel pelokalan itu membutuhkan ~ 260KB, dan Anda hanya perlu 2 diantaranya.
Dalam hal ini, Anda hanya dapat mengimpor 2 di antaranya secara langsung di file tempat AppTranslateLoader
dideklarasikan.
import 'moment/locale/en-gb'; import 'moment/locale/ru';
Sekarang lokalisasi en-gb dan ru akan ada dalam bundel aplikasi js. Di AppTranslateLoader
Anda dapat menambahkan penangan bahasa yang baru saja dimuat:
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(); // - . }
!!! pawang ini memiliki kekurangan: Jika dalam proyek kami hanya en lokalisasi disediakan untuk ngx-translate, tetapi sebagai contoh saat atau perlu menggunakan en atau en-gb, logika pawang harus diperluas, atau en-lokalisasi en-gb juga harus disediakan dalam ngx-terjemahkan.
!!! untuk saat ini dengan // TODO: kita dapat menulis plugin webpack, kita akan mempertimbangkan beberapa plugin nanti, tetapi saya belum memilikinya.
Anda bertanya, mengapa tidak mungkin memuat lokalisasi tanggal dan waktu serta pelokalan teks dalam antarmuka (secara dinamis, melalui HTTP)? Ini karena pelokalan tanggal mengandung logika mereka sendiri, dan oleh karena itu disajikan sebagai kode javascript .
Namun terlepas dari ini, ada cara untuk memuat pelokalan seperti itu dengan menulis sedikit kode 'kotor'. Saya tidak menggunakan kode ini dalam produksi, tetapi pelokalan ke-2 di dalam bundel saya tidak mengganggu saya. Tetapi jika Anda memiliki banyak pelokalan, Anda ingin memuatnya secara dinamis dan tidak terlalu aman, perlu diingat:
private async loadAngularCulture(locale) { let angularLocaleText = await this.httpClient.get(`assets/angular-locales/${locale}.js`).toPromise();
Terakhir kali saya menguji metode ini dalam Angular 4. Kemungkinan besar itu berfungsi sekarang.
Sayangnya, hack kehidupan kotor seperti itu tidak akan berfungsi dalam kasus c momentJs (hanya lokalisasi Angular). Setidaknya saya tidak bisa menemukan cara untuk melakukan ini, tetapi jika Anda seorang programmer hacker yang sangat berjanggut, saya akan senang melihat solusi dalam komentar.
3. Caching. Mirip dengan membangun bundel .js, Anda bisa menambahkan hash ke nama bundel .json.
Itu semua tergantung pada bagaimana tepatnya Anda mengumpulkan semua json ke dalam satu file, mungkin Anda hanya memiliki semuanya dalam satu file. Di Internet, Anda dapat menemukan sejumlah modul npm yang dapat mengumpulkan json kecil dalam satu file. Saya tidak menemukan orang-orang yang dapat melampirkan hash dan mengumpulkan semuanya menjadi satu file. Webpack sendiri juga tidak dapat menangani json seperti yang diminta oleh ngx-translate. Karena itu, kami akan menulis plugin webpack kami.
Singkatnya: kita perlu mengumpulkan semua json di proyek sesuai dengan pola tertentu, sementara kita perlu mengelompokkannya berdasarkan nama (en, ru, de, dll.) Karena, misalnya, en.json dapat berada di folder yang berbeda. Kemudian, untuk setiap file yang dikumpulkan, Anda harus melampirkan hash.
Ada masalah di sini. Bagaimana AppTranslateLoader
mengenali nama file jika setiap pelokalan memiliki namanya sendiri? Misalnya, termasuk bundel di index.html, kami dapat menyertakan HtmlWebpackPlugin dan memintanya untuk menambahkan tag skrip dengan nama bundel sendiri.
Untuk mengatasi masalah ini untuk pelokalan .json, plugin webpack kami akan membuat config.json, yang akan berisi asosiasi kode bahasa dengan nama file hash:
{ "en": "en.some_hash.json", "ru": "ru.some_hash.json" }
config.json juga akan di-cache oleh browser, tetapi itu akan memakan waktu sedikit dan kita dapat dengan mudah menentukan parameter queryString acak ketika GET ditumbuhi dengan file ini (sehingga terus memuatnya lagi). Atau berikan ID acak ke config.json (saya akan menjelaskan metode ini, yang pertama dapat ditemukan di Google).
Saya juga ingin sedikit menyederhanakan infrastruktur dan atomisasi lokalisasi. json dengan pelokalan akan terletak di folder dengan komponennya. Dan untuk menghindari kunci duplikat, struktur bundel json akan dibangun berdasarkan path ke file json tertentu. Sebagai contoh, kita memiliki dua en.json, satu terletak pada path src/app/article-component
, dan lainnya src/app/comment-component
. Saya ingin mendapatkan json berikut dalam output:
{ "article-component": { "TITLE": "Article title" }, "comment-component": { "TITLE": "Comment title" } }
Kita dapat membuang bagian dari jalan yang tidak kita butuhkan, sehingga kunci akan sesingkat mungkin dalam tampilan.
!!! Ada kekurangannya: ketika Anda menempatkan komponen di folder lain, kunci lokalisasi akan berubah.
Nanti kita akan mempertimbangkan hack kehidupan lain yang akan memungkinkan kita untuk menunjukkan dalam komponen hanya bidang kunci terakhir, tidak peduli di mana dan seberapa dalam proyek kita dalam proyek, dan dengan demikian kita dapat mentransfernya sesuka kita dan ganti namanya sesuka kita.
Pada dasarnya, saya ingin mencapai enkapsulasi dan bahkan sedikit polimorfisme lokalisasi ngx-translate. Saya suka konsep enkapsulasi pandangan dalam Angular - Angular View Encapsulation , atau lebih tepatnya Shadow DOM . Ya, ini meningkatkan ukuran aplikasi secara keseluruhan, tetapi saya akan mengatakan sebelumnya bahwa setelah ngx-translate menjadi lebih dienkapsulasi, bekerja dengan file lokalisasi menjadi jauh lebih menyenangkan. Komponen mulai peduli hanya tentang pelokalan mereka, di samping itu, akan mungkin untuk mendefinisikan kembali pelokalan dalam komponen anak tergantung pada pelokalan dalam komponen induk. Juga, sekarang Anda dapat mentransfer komponen dari proyek ke proyek, dan mereka sudah akan dilokalkan. Tetapi seperti di tempat lain, ada nuansa, lebih lanjut tentang itu nanti.
Jadi mari kita beralih ke plugin kita. Apa itu dan bagaimana . gabungkan plugin pelokalan .
Kode sumber untuk loader dan plugin dapat ditemukan di tautan contoh di bagian bawah artikel (folder ./build-utils).
Plugin melakukan segalanya tentang apa yang ditulis di atas, dan menerima opsi berikut:
- hilangkan. nama di jalur menuju pelokalan yang perlu diabaikan (inilah saat saya ingin menghapus bagian tambahan dari jalur ke file)
- fileInput. biasa untuk mengambil file lokalisasi di prodge (seperti tes di webpack)
- rootDir. di mana mulai mencari file dengan pola fileInput
- outputDir. di mana file config dan pelokalan akan dibuat di folder dist
- configName. dengan nama apa file konfigurasi akan dibuat.
Dalam proyek saya, plugin terhubung dengan cara ini:
Di dalam komponen yang perlu pelokalan ada folder @translations
, yang berisi en.json, ru, dll.
Akibatnya, ketika membalik, semuanya akan dikumpulkan menjadi satu file, dengan mempertimbangkan jalur ke folder @translations
. Bundel lokalisasi akan berada di dist / langs /, dan konfigurasi akan dinamai sebagai config. $ {Some-random} .json.
Selanjutnya, kami akan memastikan bahwa bundel lokalisasi yang diinginkan dimuat ke dalam aplikasi. Ada saat yang rapuh - hanya webpack yang tahu tentang lintasan ke lokasi dan nama file konfigurasi, mari kita pertimbangkan ini sehingga data terbaru masuk ke AppTranslateLoader dan tidak perlu mengubah nama di dua tempat.
// 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
tidak akan berfungsi, kita perlu mendeklarasikan plugin lain di webpack (DefinePlugin atau EnvironmentPlugin):
Sekarang kita dapat mengubah jalur ke pelokalan dan nama konfigurasi hanya di satu tempat.
Secara default, dari penjualan Angular default yang dihasilkan dalam rakitan webpack (misalnya), Anda tidak dapat menentukan process.env.someValue
dari kode (bahkan jika Anda menggunakan DefinePlugin), kompiler dapat bersumpah. Agar ini berfungsi, Anda harus memenuhi 2a kondisi:
- di main.ts tambahkan baris pertama
/// <reference types="node"/>
- package.json harus memiliki
@types/node
- npm install --save-dev @types/node
.
Kami melanjutkan langsung ke proses boot.
Jika Anda berniat menggunakan APP_INITIALIZER, pastikan untuk mengembalikan Janji, tidak Dapat Diamati. Tugas kami adalah menulis rantai permintaan:
- Pertama, Anda perlu mengunduh config.json (hanya jika tidak dimuat).
- coba muat bahasa, yang merupakan bahasa browser pengguna
- Berikan logika fallback dengan bahasa unduhan default.
// 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); } }
Selesai
Sekarang kembali ke masalah memindahkan komponen ke folder lain, enkapsulasi dan kemiripan polimorfisme.
Faktanya, kita sudah memiliki semacam enkapsulasi. Pelokalan didorong ke folder di sebelah komponen, semua jalur kunci unik, tetapi kita masih bisa melokalkan kunci komponen beberapa komponen1 di dalam beberapa komponen2 dan akan sulit untuk melacak semuanya, kita akan mengetahuinya nanti.
<some-component1 [someLabel]="'components.some-component2.some_key' | tanslate"></some-component1> // components.some-component2 -
Mengenai pergerakan komponen:
Sekarang kunci yang akan kita gunakan dalam tampilan terikat dengan kaku pada jalur relatif ke file pelokalan dan tergantung pada infrastruktur spesifik proyek.
Saya akan memberikan kasus yang agak menyedihkan dari situasi ini:
<div translate="+lazy-module.components.article-component.article_title"></div>
Tetapi bagaimana jika saya mengubah nama folder komponen ke post-komponen?
Akan sangat sulit untuk memasukkan kunci ini di semua tempat yang diperlukan. Tentu saja, tidak ada yang membatalkan copy-paste dan mencari-ganti, tetapi menulis ini tanpa diminta IDE juga membuat stres.
Untuk mengatasi masalah ini, mari kita perhatikan apa yang dilakukan webpack tentang hal ini? Webpack memiliki sesuatu yang disebut loader , ada banyak loader yang tersedia yang beroperasi pada jalur file: misalnya, jalur sumber daya dalam css - berkat webpack kita dapat menentukan jalur gambar latar relatif: url (../ relative.png), dan sebagainya sisa jalur file dalam proyek ada di mana-mana!
Siapa pun yang membuat build webpack mereka tahu bahwa loader menerima file pada input yang cocok dengan pola tertentu. Tugas dari loader itu sendiri adalah bagaimana mengubah file input ini dan mengembalikannya, untuk perubahan lebih lanjut oleh loader lainnya.
Karena itu, kita perlu menulis loader kita. Pertanyaannya adalah jenis file apa yang akan kita ubah: tampilan atau komponen? Di satu sisi, tampilan dapat langsung di komponen dan secara terpisah. Tampilan bisa cukup besar dan sulit untuk diurai, bayangkan jika kita memiliki pandangan di mana 100 menerjemahkan arahan (bukan dalam satu lingkaran):
<div id="1">{{'./some_key_1' | translate}}</div> ... <div id="100">{{'../another_key_!' | translate}}</div>
melalui loader, kami dapat mengganti jalur utama ke lokasi komponen di dekat setiap pipa atau arahan.
<div id="1">{{'app.some-component.some_key_1' | translate}}</div>
kita bisa menambahkan bidang ke komponen yang menyediakan lokalisasi:
@Component({ selector: 'app-some', template: '<div>{{(localization + 'key') | tanslate}}</div>' }) export class SomeComponent { localization = './' }
Itu juga buruk - Anda harus membuat kunci pelokalan di mana-mana.
Karena opsi yang paling jelas terlihat buruk, coba gunakan dekorator dan simpan beberapa metadata dalam prototipe komponen (seperti yang dilakukan Angular).

anotasi - metadata untuk dekorator Angular
__app_annotations__ - metadata yang akan kami simpan untuk kami sendiri
Jalur ke folder lokalisasi relatif ke komponen dapat ditulis ke dekorator, dekorator yang sama dapat diperluas dengan opsi lain kecuali jalur.
//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 .